From dffa66711eb6fb969218c520ea1d18fe0d7d2466 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 17 May 2024 02:38:11 +0000 Subject: [PATCH 01/48] share xhr, batch session recordings --- src/mixpanel-core.js | 73 +++++++++----------------------------- src/recorder/index.js | 61 +++++++++++++++++++++++++++++--- src/request-batcher.js | 44 +++++++++++------------ src/utils.js | 80 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 171 insertions(+), 87 deletions(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 4de12093..e77027a6 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -1,6 +1,6 @@ /* eslint camelcase: "off" */ import Config from './config'; -import { MAX_RECORDING_MS, _, console, userAgent, window, document, navigator, slice } from './utils'; +import { MAX_RECORDING_MS, _, console, userAgent, window, document, navigator, slice, make_xhr_request} from './utils'; import { FormTracker, LinkTracker } from './dom-trackers'; import { RequestBatcher } from './request-batcher'; import { MixpanelGroup } from './mixpanel-group'; @@ -633,62 +633,18 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data, + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -779,7 +735,10 @@ MixpanelLib.prototype.init_batchers = function() { return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/src/recorder/index.js b/src/recorder/index.js index dd573a65..e36fec27 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -1,11 +1,16 @@ import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js'; -import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase +import { MAX_RECORDING_MS, console_with_prefix, _, make_xhr_request } from '../utils'; // eslint-disable-line camelcase import { addOptOutCheckMixpanelLib } from '../gdpr-utils'; +import { RequestBatcher } from '../request-batcher'; var logger = console_with_prefix('recorder'); var CompressionStream = window['CompressionStream']; +var BATCH_SIZE = 1000; +var BATCH_FLUSH_INTERVAL_MS = 10 * 1000; +var BATCH_REQUEST_TIMEOUT_MS = 90 * 1000; + var MixpanelRecorder = function(mixpanelInstance) { this._mixpanel = mixpanelInstance; @@ -24,6 +29,20 @@ var MixpanelRecorder = function(mixpanelInstance) { this.maxTimeoutId = null; this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); +}; + + +MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + batchSize: BATCH_SIZE, + flushIntervalMs: BATCH_FLUSH_INTERVAL_MS, + requestTimeoutMs: BATCH_REQUEST_TIMEOUT_MS, + autoStart: true, + sendRequestFunc: _.bind(function(data, options, callback) { + this.sendRequestWithOptOut(data, options, callback); + }, this), + }); }; // eslint-disable-next-line camelcase @@ -52,6 +71,8 @@ MixpanelRecorder.prototype.startRecording = function () { this.replayId = _.UUID(); this.replayLengthMs = 0; + this.batcher.start(); + var resetIdleTimeout = _.bind(function () { clearTimeout(this.idleTimeoutId); this.idleTimeoutId = setTimeout(_.bind(function () { @@ -62,7 +83,7 @@ MixpanelRecorder.prototype.startRecording = function () { this._stopRecording = record({ 'emit': _.bind(function (ev) { - this.recEvents.push(ev); + this.batcher.enqueue(ev); this.replayLengthMs = new Date().getTime() - this.replayStartTime; resetIdleTimeout(); }, this), @@ -75,7 +96,6 @@ MixpanelRecorder.prototype.startRecording = function () { resetIdleTimeout(); - this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000); this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); }; @@ -90,10 +110,9 @@ MixpanelRecorder.prototype.stopRecording = function () { this._stopRecording = null; } - this._flushEvents(); // flush any remaining events + this.batcher.flush(); // flush any remaining events this.replayId = null; - clearInterval(this.sendBatchId); clearTimeout(this.idleTimeoutId); clearTimeout(this.maxTimeoutId); }; @@ -106,6 +125,10 @@ MixpanelRecorder.prototype.flushEventsWithOptOut = function () { this._flushEvents(_.bind(this._onOptOut, this)); }; +MixpanelRecorder.prototype.sendRequestWithOptOut = function (data, options, cb) { + this._sendRequest(data, options, cb, _.bind(this._onOptOut, this)); +}; + MixpanelRecorder.prototype._onOptOut = function (code) { // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out if (code === 0) { @@ -170,4 +193,32 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() { } }); +MixpanelRecorder.prototype._sendRequest = function (data, options, callback) { + var url = this.get_config('api_host') + '/' + this.get_config('api_routes')['record']; + var headers = { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/json' + }; + + var reqBody = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'events': data, + 'seq': this.seqNo++, + 'batch_start_time': this.batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': this.replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + + var reqOptions = _.extend({}, options, { + method: 'POST', + url: url, + 'body_data': JSON.stringify(reqBody), + headers: headers, + callback: callback, + }); + + make_xhr_request(reqOptions); +}; + window['__mp_recorder'] = MixpanelRecorder; diff --git a/src/request-batcher.js b/src/request-batcher.js index e9710113..2cd29588 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -14,22 +14,19 @@ var logger = console_with_prefix('batch'); * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), storage: options.storage }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -40,7 +37,7 @@ var RequestBatcher = function(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -72,26 +69,26 @@ RequestBatcher.prototype.clear = function() { }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -114,16 +111,16 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -229,7 +226,7 @@ RequestBatcher.prototype.flush = function(options) { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -271,8 +268,7 @@ RequestBatcher.prototype.flush = function(options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -284,12 +280,12 @@ RequestBatcher.prototype.flush = function(options) { */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } diff --git a/src/utils.js b/src/utils.js index af6a76db..63454c0c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1674,6 +1674,83 @@ var cheap_guid = function(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; + +/** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ +var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); +}; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -1737,5 +1814,6 @@ export { localStorageSupported, JSONStringify, JSONParse, - slice + slice, + make_xhr_request, }; From 7c28937e44a9a6067b00625279bd714e7f96cb3e Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 17 May 2024 14:41:40 +0000 Subject: [PATCH 02/48] only use memory persistence for now --- src/recorder/index.js | 14 ++++++++++---- src/request-batcher.js | 4 ++-- src/request-queue.js | 36 +++++++++++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index e36fec27..75b2e4b9 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -121,10 +121,6 @@ MixpanelRecorder.prototype.stopRecording = function () { * Flushes the current batch of events to the server, but passes an opt-out callback to make sure * we stop recording and dump any queued events if the user has opted out. */ -MixpanelRecorder.prototype.flushEventsWithOptOut = function () { - this._flushEvents(_.bind(this._onOptOut, this)); -}; - MixpanelRecorder.prototype.sendRequestWithOptOut = function (data, options, cb) { this._sendRequest(data, options, cb, _.bind(this._onOptOut, this)); }; @@ -210,6 +206,16 @@ MixpanelRecorder.prototype._sendRequest = function (data, options, callback) { 'replay_start_time': this.replayStartTime / 1000 }; + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqBody['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqBody['$user_id'] = userId; + } + var reqOptions = _.extend({}, options, { method: 'POST', url: url, diff --git a/src/request-batcher.js b/src/request-batcher.js index 2cd29588..429fb5d5 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -18,10 +18,10 @@ var RequestBatcher = function(storageKey, options) { this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence, }); - // seed variable batch size + flush interval with configured values this.currentBatchSize = this.options.batchSize; this.currentFlushInterval = this.options.flushIntervalMs; diff --git a/src/request-queue.js b/src/request-queue.js index b36515c5..c2b428d6 100644 --- a/src/request-queue.js +++ b/src/request-queue.js @@ -26,6 +26,7 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger.error, logger); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -50,6 +51,17 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; + if (this.usePersistence) { + this._enqueuePersisted(queueEntry, cb); + return; + } + + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } +}; +RequestQueue.prototype._enqueuePersisted = function (queueEntry, cb) { this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -75,6 +87,8 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { }, this), this.pid); }; + + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -83,7 +97,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -132,7 +146,13 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -214,6 +234,13 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -275,7 +302,10 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; export { RequestQueue }; From b3bfa670df390c485b341f8a66dc85cda631995c Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 17 May 2024 15:51:25 +0000 Subject: [PATCH 03/48] cleanup, draft --- dist/mixpanel-recorder.js | 973 ++++++++++++++++++++++++- dist/mixpanel-recorder.min.js | 23 + dist/mixpanel.amd.js | 246 ++++--- dist/mixpanel.cjs.js | 246 ++++--- dist/mixpanel.globals.js | 246 ++++--- dist/mixpanel.min.js | 113 +++ dist/mixpanel.umd.js | 246 ++++--- examples/commonjs-browserify/bundle.js | 246 ++++--- examples/es2015-babelify/bundle.js | 236 +++--- examples/umd-webpack/bundle.js | 246 ++++--- src/mixpanel-core.js | 5 +- src/recorder/index.js | 51 +- src/request-batcher.js | 12 +- src/request-queue.js | 14 +- 14 files changed, 2262 insertions(+), 641 deletions(-) diff --git a/dist/mixpanel-recorder.js b/dist/mixpanel-recorder.js index f8f434fd..eca2171c 100644 --- a/dist/mixpanel-recorder.js +++ b/dist/mixpanel-recorder.js @@ -4478,8 +4478,13 @@ record.mirror = mirror; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -4542,12 +4547,41 @@ var console$1 = { /** @type {function(...*)} */ log: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } }, /** @type {function(...*)} */ warn: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); + try { + windowConsole.warn.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.warn(arg); + }); + } + } }, /** @type {function(...*)} */ error: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } }, /** @type {function(...*)} */ critical: function() { @@ -6128,6 +6162,83 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; + + /** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ + var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); + }; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -6344,9 +6455,760 @@ }; } + var logger$3 = console_with_prefix('lock'); + + /** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ + var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; + }; + + // pass in a specific pid to test contention scenarios; otherwise + // it is chosen randomly for each acquisition attempt + SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } + }; + + var logger$2 = console_with_prefix('batch'); + + /** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ + var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.usePersistence = options.usePersistence; + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; + }; + + /** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ + RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + }; + + + /** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ + RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (this.usePersistence && batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; + }; + + /** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ + var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; + }; + + /** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ + RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); + }; + + // internal helper for RequestQueue.updatePayloads + var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; + }; + + /** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ + RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + }; + + /** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ + RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; + }; + + /** + * Serialize the given items array to localStorage. + */ + RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } + }; + + /** + * Clear out queues (memory and localStorage). + */ + RequestQueue.prototype.clear = function() { + this.memQueue = []; + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } + }; + + // maximum interval between request retries after exponential backoff + var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + + var logger$1 = console_with_prefix('batch'); + + /** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ + var RequestBatcher = function(storageKey, options) { + this.options = options; + + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage, + usePersistence: options.usePersistence + }); + + // seed variable batch size + flush interval with configured values + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; + + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; + }; + + /** + * Add one item to queue. + */ + RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.currentFlushInterval, cb); + }; + + /** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ + RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); + }; + + /** + * Stop flushing batches. Can be restarted by calling start(). + */ + RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } + }; + + /** + * Clear out queue. + */ + RequestBatcher.prototype.clear = function() { + this.queue.clear(); + }; + + /** + * Restore batch size configuration to the originally initialized value + */ + RequestBatcher.prototype.resetBatchSize = function() { + this.currentBatchSize = this.options.batchSize; + }; + + /** + * Restore flush interval time configuration to the originally initialized value + */ + RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.options.flushIntervalMs); + }; + + /** + * Schedule the next flush in the given number of milliseconds. + */ + RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.currentFlushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + } + }; + + /** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ + RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger$1.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.options.requestTimeoutMs; + var startTime = new Date().getTime(); + var currentBatchSize = this.currentBatchSize; + var batch = this.queue.fillBatch(currentBatchSize); + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + res.xhr_req && + (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + var headers = res.xhr_req['responseHeaders']; + if (headers) { + var retryAfter = headers['Retry-After']; + if (retryAfter) { + retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; + } + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.options.stopAllBatchingFunc(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger$1.log('MIXPANEL REQUEST:', dataForRequest); + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } + }; + + /** + * Log error to global logger and optional user-defined logger. + */ + RequestBatcher.prototype.reportError = function(msg, err) { + logger$1.error.apply(logger$1.error, arguments); + if (this.options.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.options.errorReporter(msg, err); + } catch(err) { + logger$1.error(err); + } + } + }; + var logger = console_with_prefix('recorder'); var CompressionStream = window['CompressionStream']; + var BATCH_SIZE = 1000; + var BATCH_FLUSH_INTERVAL_MS = 10 * 1000; + var BATCH_REQUEST_TIMEOUT_MS = 90 * 1000; + var MixpanelRecorder = function(mixpanelInstance) { this._mixpanel = mixpanelInstance; @@ -6365,6 +7227,38 @@ this.maxTimeoutId = null; this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); + }; + + + MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + batchSize: BATCH_SIZE, + flushIntervalMs: BATCH_FLUSH_INTERVAL_MS, + requestTimeoutMs: BATCH_REQUEST_TIMEOUT_MS, + autoStart: true, + sendRequestFunc: _.bind(function(data, options, callback) { + this.sendRequestWithOptOut(data, options, callback); + }, this), + forceDelayFlush: true, + }); + + // var flushOnUnload = _.bind(function() { + // if (!this.batcher.stopped) { + // this.batcher.flush({unloading: true}); + // } + // }, this); + + // window.addEventListener('pagehide', function(ev) { + // if (ev['persisted']) { + // flushOnUnload(); + // } + // }); + // window.addEventListener('visibilitychange', function() { + // if (document['visibilityState'] === 'hidden') { + // flushOnUnload(); + // } + // }); }; // eslint-disable-next-line camelcase @@ -6393,6 +7287,8 @@ this.replayId = _.UUID(); this.replayLengthMs = 0; + this.batcher.start(); + var resetIdleTimeout = _.bind(function () { clearTimeout(this.idleTimeoutId); this.idleTimeoutId = setTimeout(_.bind(function () { @@ -6403,7 +7299,7 @@ this._stopRecording = record({ 'emit': _.bind(function (ev) { - this.recEvents.push(ev); + this.batcher.enqueue(ev); this.replayLengthMs = new Date().getTime() - this.replayStartTime; resetIdleTimeout(); }, this), @@ -6416,7 +7312,6 @@ resetIdleTimeout(); - this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000); this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); }; @@ -6431,10 +7326,9 @@ this._stopRecording = null; } - this._flushEvents(); // flush any remaining events + this.batcher.flush(); // flush any remaining events this.replayId = null; - clearInterval(this.sendBatchId); clearTimeout(this.idleTimeoutId); clearTimeout(this.maxTimeoutId); }; @@ -6443,8 +7337,8 @@ * Flushes the current batch of events to the server, but passes an opt-out callback to make sure * we stop recording and dump any queued events if the user has opted out. */ - MixpanelRecorder.prototype.flushEventsWithOptOut = function () { - this._flushEvents(_.bind(this._onOptOut, this)); + MixpanelRecorder.prototype.sendRequestWithOptOut = function (data, options, cb) { + this._sendRequest(data, options, cb, _.bind(this._onOptOut, this)); }; MixpanelRecorder.prototype._onOptOut = function (code) { @@ -6455,6 +7349,7 @@ } }; +<<<<<<< HEAD MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) { window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { 'method': 'POST', @@ -6508,9 +7403,75 @@ reqParams['format'] = 'body'; this._sendRequest(reqParams, eventsJson); } +======= + MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (data, options, callback) { + var url = this.get_config('api_host') + '/' + this.get_config('api_routes')['record']; + var headers = { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/json' + }; + + var reqBody = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'events': data, + 'seq': this.seqNo++, + 'batch_start_time': this.batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': this.replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqBody['$device_id'] = deviceId; +>>>>>>> 571d8b5 (cleanup, draft) + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqBody['$user_id'] = userId; } + + var bodyData = _.JSONEncode(reqBody); + + // if (options.transport === 'sendBeacon') { + // // we have access to fetch in this environment due to MutationObserver requirement. + // // use it as a replacement for sendBeacon so that we can set header authorization. + // window['fetch'](url, { + // method: 'POST', + // headers: headers, + // body: bodyData, + // keepalive: true, + // }); + // callback(true); + // } + + var reqOptions = _.extend({}, options, { + method: 'POST', + url: url, + 'body_data': bodyData, + headers: headers, + callback: callback, + reportError: _.bind(this.reportError, this) + }); + + make_xhr_request(reqOptions); }); + + MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } + }; + + window['__mp_recorder'] = MixpanelRecorder; })(); diff --git a/dist/mixpanel-recorder.min.js b/dist/mixpanel-recorder.min.js index 9cb0d896..7f1887ab 100644 --- a/dist/mixpanel-recorder.min.js +++ b/dist/mixpanel-recorder.min.js @@ -1,6 +1,12 @@ +<<<<<<< HEAD (function(){"use strict";var A;(function(e){e[e.Document=0]="Document",e[e.DocumentType=1]="DocumentType",e[e.Element=2]="Element",e[e.Text=3]="Text",e[e.CDATA=4]="CDATA",e[e.Comment=5]="Comment"})(A||(A={}));function ar(e){return e.nodeType===e.ELEMENT_NODE}function ge(e){const t=e?.host;return t?.shadowRoot===e}function ye(e){return Object.prototype.toString.call(e)==="[object ShadowRoot]"}function lr(e){return e.includes(" background-clip: text;")&&!e.includes(" -webkit-background-clip: text;")&&(e=e.replace(" background-clip: text;"," -webkit-background-clip: text; background-clip: text;")),e}function cr(e){const{cssText:t}=e;if(t.split('"').length<3)return t;const r=["@import",`url(${JSON.stringify(e.href)})`];return e.layerName===""?r.push("layer"):e.layerName&&r.push(`layer(${e.layerName})`),e.supportsText&&r.push(`supports(${e.supportsText})`),e.media.length&&r.push(e.media.mediaText),r.join(" ")+";"}function _e(e){try{const t=e.rules||e.cssRules;return t?lr(Array.from(t,ft).join("")):null}catch{return null}}function ft(e){let t;if(dr(e))try{t=_e(e.styleSheet)||cr(e)}catch{}else if(fr(e)&&e.selectorText.includes(":"))return ur(e.cssText);return t||e.cssText}function ur(e){const t=/(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;return e.replace(t,"$1\\$2")}function dr(e){return"styleSheet"in e}function fr(e){return"selectorText"in e}class ht{constructor(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}getId(t){var r;if(!t)return-1;const n=(r=this.getMeta(t))===null||r===void 0?void 0:r.id;return n??-1}getNode(t){return this.idNodeMap.get(t)||null}getIds(){return Array.from(this.idNodeMap.keys())}getMeta(t){return this.nodeMetaMap.get(t)||null}removeNodeFromMap(t){const r=this.getId(t);this.idNodeMap.delete(r),t.childNodes&&t.childNodes.forEach(n=>this.removeNodeFromMap(n))}has(t){return this.idNodeMap.has(t)}hasNode(t){return this.nodeMetaMap.has(t)}add(t,r){const n=r.id;this.idNodeMap.set(n,t),this.nodeMetaMap.set(t,r)}replace(t,r){const n=this.getNode(t);if(n){const i=this.nodeMetaMap.get(n);i&&this.nodeMetaMap.set(r,i)}this.idNodeMap.set(t,r)}reset(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}}function hr(){return new ht}function Ve({element:e,maskInputOptions:t,tagName:r,type:n,value:i,maskInputFn:o}){let a=i||"";const l=n&&te(n);return(t[r.toLowerCase()]||l&&t[l])&&(o?a=o(a,e):a="*".repeat(a.length)),a}function te(e){return e.toLowerCase()}const pt="__rrweb_original__";function pr(e){const t=e.getContext("2d");if(!t)return!0;const r=50;for(let n=0;ns!==0))return!1}return!0}function qe(e){const t=e.type;return e.hasAttribute("data-rr-is-password")?"password":t?te(t):null}function mt(e,t){var r;let n;try{n=new URL(e,t??window.location.href)}catch{return null}const i=/\.([0-9a-z]+)(?:$)/i,o=n.pathname.match(i);return(r=o?.[1])!==null&&r!==void 0?r:null}let mr=1;const gr=new RegExp("[^a-z0-9-_:]"),Se=-2;function gt(){return mr++}function yr(e){if(e instanceof HTMLFormElement)return"form";const t=te(e.tagName);return gr.test(t)?"div":t}function Sr(e){let t="";return e.indexOf("//")>-1?t=e.split("/").slice(0,3).join("/"):t=e.split("/")[0],t=t.split("?")[0],t}let ce,yt;const vr=/url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm,br=/^(?:[a-z+]+:)?\/\//i,wr=/^www\..*/i,Mr=/^(data:)([^,]*),(.*)/i;function xe(e,t){return(e||"").replace(vr,(r,n,i,o,a,l)=>{const s=i||a||l,c=n||o||"";if(!s)return r;if(br.test(s)||wr.test(s))return`url(${c}${s}${c})`;if(Mr.test(s))return`url(${c}${s}${c})`;if(s[0]==="/")return`url(${c}${Sr(t)+s}${c})`;const u=t.split("/"),p=s.split("/");u.pop();for(const m of p)m!=="."&&(m===".."?u.pop():u.push(m));return`url(${c}${u.join("/")}${c})`})}const Ir=/^[^ \t\n\r\u000c]+/,Cr=/^[, \t\n\r\u000c]+/;function Or(e,t){if(t.trim()==="")return t;let r=0;function n(o){let a;const l=o.exec(t.substring(r));return l?(a=l[0],r+=a.length,a):""}const i=[];for(;n(Cr),!(r>=t.length);){let o=n(Ir);if(o.slice(-1)===",")o=ue(e,o.substring(0,o.length-1)),i.push(o);else{let a="";o=ue(e,o);let l=!1;for(;;){const s=t.charAt(r);if(s===""){i.push((o+a).trim());break}else if(l)s===")"&&(l=!1);else if(s===","){r+=1,i.push((o+a).trim());break}else s==="("&&(l=!0);a+=s,r+=1}}}return i.join(", ")}function ue(e,t){if(!t||t.trim()==="")return t;const r=e.createElement("a");return r.href=t,r.href}function _r(e){return!!(e.tagName==="svg"||e.ownerSVGElement)}function Je(){const e=document.createElement("a");return e.href="",e.href}function St(e,t,r,n){return n&&(r==="src"||r==="href"&&!(t==="use"&&n[0]==="#")||r==="xlink:href"&&n[0]!=="#"||r==="background"&&(t==="table"||t==="td"||t==="th")?ue(e,n):r==="srcset"?Or(e,n):r==="style"?xe(n,Je()):t==="object"&&r==="data"?ue(e,n):n)}function vt(e,t,r){return(e==="video"||e==="audio")&&t==="autoplay"}function xr(e,t,r){try{if(typeof t=="string"){if(e.classList.contains(t))return!0}else for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}if(r)return e.matches(r)}catch{}return!1}function Ee(e,t,r){if(!e)return!1;if(e.nodeType!==e.ELEMENT_NODE)return r?Ee(e.parentNode,t,r):!1;for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}return r?Ee(e.parentNode,t,r):!1}function bt(e,t,r,n){try{const i=e.nodeType===e.ELEMENT_NODE?e:e.parentElement;if(i===null)return!1;if(typeof t=="string"){if(n){if(i.closest(`.${t}`))return!0}else if(i.classList.contains(t))return!0}else if(Ee(i,t,n))return!0;if(r){if(n){if(i.closest(r))return!0}else if(i.matches(r))return!0}}catch{}return!1}function Er(e,t,r){const n=e.contentWindow;if(!n)return;let i=!1,o;try{o=n.document.readyState}catch{return}if(o!=="complete"){const l=setTimeout(()=>{i||(t(),i=!0)},r);e.addEventListener("load",()=>{clearTimeout(l),i=!0,t()});return}const a="about:blank";if(n.location.href!==a||e.src===a||e.src==="")return setTimeout(t,0),e.addEventListener("load",t);e.addEventListener("load",t)}function kr(e,t,r){let n=!1,i;try{i=e.sheet}catch{return}if(i)return;const o=setTimeout(()=>{n||(t(),n=!0)},r);e.addEventListener("load",()=>{clearTimeout(o),n=!0,t()})}function Tr(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:a,inlineStylesheet:l,maskInputOptions:s={},maskTextFn:c,maskInputFn:u,dataURLOptions:p={},inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:h=!1}=t,y=Rr(r,n);switch(e.nodeType){case e.DOCUMENT_NODE:return e.compatMode!=="CSS1Compat"?{type:A.Document,childNodes:[],compatMode:e.compatMode}:{type:A.Document,childNodes:[]};case e.DOCUMENT_TYPE_NODE:return{type:A.DocumentType,name:e.name,publicId:e.publicId,systemId:e.systemId,rootId:y};case e.ELEMENT_NODE:return Dr(e,{doc:r,blockClass:i,blockSelector:o,inlineStylesheet:l,maskInputOptions:s,maskInputFn:u,dataURLOptions:p,inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:h,rootId:y});case e.TEXT_NODE:return Nr(e,{needsMask:a,maskTextFn:c,rootId:y});case e.CDATA_SECTION_NODE:return{type:A.CDATA,textContent:"",rootId:y};case e.COMMENT_NODE:return{type:A.Comment,textContent:e.textContent||"",rootId:y};default:return!1}}function Rr(e,t){if(!t.hasNode(e))return;const r=t.getId(e);return r===1?void 0:r}function Nr(e,t){var r;const{needsMask:n,maskTextFn:i,rootId:o}=t,a=e.parentNode&&e.parentNode.tagName;let l=e.textContent;const s=a==="STYLE"?!0:void 0,c=a==="SCRIPT"?!0:void 0;if(s&&l){try{e.nextSibling||e.previousSibling||!((r=e.parentNode.sheet)===null||r===void 0)&&r.cssRules&&(l=_e(e.parentNode.sheet))}catch(u){console.warn(`Cannot get CSS styles from text's parentNode. Error: ${u}`,e)}l=xe(l,Je())}return c&&(l="SCRIPT_PLACEHOLDER"),!s&&!c&&l&&n&&(l=i?i(l,e.parentElement):l.replace(/[\S]/g,"*")),{type:A.Text,textContent:l||"",isStyle:s,rootId:o}}function Dr(e,t){const{doc:r,blockClass:n,blockSelector:i,inlineStylesheet:o,maskInputOptions:a={},maskInputFn:l,dataURLOptions:s={},inlineImages:c,recordCanvas:u,keepIframeSrcFn:p,newlyAddedElement:m=!1,rootId:f}=t,g=xr(e,n,i),h=yr(e);let y={};const w=e.attributes.length;for(let S=0;SM.href===e.href);let b=null;S&&(b=_e(S)),b&&(delete y.rel,delete y.href,y._cssText=xe(b,S.href))}if(h==="style"&&e.sheet&&!(e.innerText||e.textContent||"").trim().length){const S=_e(e.sheet);S&&(y._cssText=xe(S,Je()))}if(h==="input"||h==="textarea"||h==="select"){const S=e.value,b=e.checked;y.type!=="radio"&&y.type!=="checkbox"&&y.type!=="submit"&&y.type!=="button"&&S?y.value=Ve({element:e,type:qe(e),tagName:h,value:S,maskInputOptions:a,maskInputFn:l}):b&&(y.checked=b)}if(h==="option"&&(e.selected&&!a.select?y.selected=!0:delete y.selected),h==="canvas"&&u){if(e.__context==="2d")pr(e)||(y.rr_dataURL=e.toDataURL(s.type,s.quality));else if(!("__context"in e)){const S=e.toDataURL(s.type,s.quality),b=document.createElement("canvas");b.width=e.width,b.height=e.height;const M=b.toDataURL(s.type,s.quality);S!==M&&(y.rr_dataURL=S)}}if(h==="img"&&c){ce||(ce=r.createElement("canvas"),yt=ce.getContext("2d"));const S=e,b=S.crossOrigin;S.crossOrigin="anonymous";const M=()=>{S.removeEventListener("load",M);try{ce.width=S.naturalWidth,ce.height=S.naturalHeight,yt.drawImage(S,0,0),y.rr_dataURL=ce.toDataURL(s.type,s.quality)}catch(F){console.warn(`Cannot inline img src=${S.currentSrc}! Error: ${F}`)}b?y.crossOrigin=b:S.removeAttribute("crossorigin")};S.complete&&S.naturalWidth!==0?M():S.addEventListener("load",M)}if(h==="audio"||h==="video"){const S=y;S.rr_mediaState=e.paused?"paused":"played",S.rr_mediaCurrentTime=e.currentTime,S.rr_mediaPlaybackRate=e.playbackRate,S.rr_mediaMuted=e.muted,S.rr_mediaLoop=e.loop,S.rr_mediaVolume=e.volume}if(m||(e.scrollLeft&&(y.rr_scrollLeft=e.scrollLeft),e.scrollTop&&(y.rr_scrollTop=e.scrollTop)),g){const{width:S,height:b}=e.getBoundingClientRect();y={class:y.class,rr_width:`${S}px`,rr_height:`${b}px`}}h==="iframe"&&!p(y.src)&&(e.contentDocument||(y.rr_src=y.src),delete y.src);let v;try{customElements.get(h)&&(v=!0)}catch{}return{type:A.Element,tagName:h,attributes:y,childNodes:[],isSVG:_r(e)||void 0,needBlock:g,rootId:f,isCustom:v}}function E(e){return e==null?"":e.toLowerCase()}function Ar(e,t){if(t.comment&&e.type===A.Comment)return!0;if(e.type===A.Element){if(t.script&&(e.tagName==="script"||e.tagName==="link"&&(e.attributes.rel==="preload"||e.attributes.rel==="modulepreload")&&e.attributes.as==="script"||e.tagName==="link"&&e.attributes.rel==="prefetch"&&typeof e.attributes.href=="string"&&mt(e.attributes.href)==="js"))return!0;if(t.headFavicon&&(e.tagName==="link"&&e.attributes.rel==="shortcut icon"||e.tagName==="meta"&&(E(e.attributes.name).match(/^msapplication-tile(image|color)$/)||E(e.attributes.name)==="application-name"||E(e.attributes.rel)==="icon"||E(e.attributes.rel)==="apple-touch-icon"||E(e.attributes.rel)==="shortcut icon")))return!0;if(e.tagName==="meta"){if(t.headMetaDescKeywords&&E(e.attributes.name).match(/^description|keywords$/))return!0;if(t.headMetaSocial&&(E(e.attributes.property).match(/^(og|twitter|fb):/)||E(e.attributes.name).match(/^(og|twitter):/)||E(e.attributes.name)==="pinterest"))return!0;if(t.headMetaRobots&&(E(e.attributes.name)==="robots"||E(e.attributes.name)==="googlebot"||E(e.attributes.name)==="bingbot"))return!0;if(t.headMetaHttpEquiv&&e.attributes["http-equiv"]!==void 0)return!0;if(t.headMetaAuthorship&&(E(e.attributes.name)==="author"||E(e.attributes.name)==="generator"||E(e.attributes.name)==="framework"||E(e.attributes.name)==="publisher"||E(e.attributes.name)==="progid"||E(e.attributes.property).match(/^article:/)||E(e.attributes.property).match(/^product:/)))return!0;if(t.headMetaVerification&&(E(e.attributes.name)==="google-site-verification"||E(e.attributes.name)==="yandex-verification"||E(e.attributes.name)==="csrf-token"||E(e.attributes.name)==="p:domain_verify"||E(e.attributes.name)==="verify-v1"||E(e.attributes.name)==="verification"||E(e.attributes.name)==="shopify-checkout-api-token"))return!0}}return!1}function de(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,maskTextClass:a,maskTextSelector:l,skipChild:s=!1,inlineStylesheet:c=!0,maskInputOptions:u={},maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g={},inlineImages:h=!1,recordCanvas:y=!1,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S=5e3,onStylesheetLoad:b,stylesheetLoadTimeout:M=5e3,keepIframeSrcFn:F=()=>!1,newlyAddedElement:P=!1}=t;let{needsMask:k}=t,{preserveWhiteSpace:T=!0}=t;!k&&e.childNodes&&(k=bt(e,a,l,k===void 0));const j=Tr(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,dataURLOptions:g,inlineImages:h,recordCanvas:y,keepIframeSrcFn:F,newlyAddedElement:P});if(!j)return console.warn(e,"not serialized"),null;let V;n.hasNode(e)?V=n.getId(e):Ar(j,f)||!T&&j.type===A.Text&&!j.isStyle&&!j.textContent.replace(/^\s+|\s+$/gm,"").length?V=Se:V=gt();const x=Object.assign(j,{id:V});if(n.add(e,x),V===Se)return null;w&&w(e);let oe=!s;if(x.type===A.Element){oe=oe&&!x.needBlock,delete x.needBlock;const H=e.shadowRoot;H&&ye(H)&&(x.isShadowHost=!0)}if((x.type===A.Document||x.type===A.Element)&&oe){f.headWhitespace&&x.type===A.Element&&x.tagName==="head"&&(T=!1);const H={doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:s,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F};if(!(x.type===A.Element&&x.tagName==="textarea"&&x.attributes.value!==void 0))for(const ee of Array.from(e.childNodes)){const K=de(ee,H);K&&x.childNodes.push(K)}if(ar(e)&&e.shadowRoot)for(const ee of Array.from(e.shadowRoot.childNodes)){const K=de(ee,H);K&&(ye(e.shadowRoot)&&(K.isShadow=!0),x.childNodes.push(K))}}return e.parentNode&&ge(e.parentNode)&&ye(e.parentNode)&&(x.isShadow=!0),x.type===A.Element&&x.tagName==="iframe"&&Er(e,()=>{const H=e.contentDocument;if(H&&v){const ee=de(H,{doc:H,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F});ee&&v(e,ee)}},S),x.type===A.Element&&x.tagName==="link"&&typeof x.attributes.rel=="string"&&(x.attributes.rel==="stylesheet"||x.attributes.rel==="preload"&&typeof x.attributes.href=="string"&&mt(x.attributes.href)==="css")&&kr(e,()=>{if(b){const H=de(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F});H&&b(e,H)}},M),x}function Lr(e,t){const{mirror:r=new ht,blockClass:n="rr-block",blockSelector:i=null,maskTextClass:o="rr-mask",maskTextSelector:a=null,inlineStylesheet:l=!0,inlineImages:s=!1,recordCanvas:c=!1,maskAllInputs:u=!1,maskTextFn:p,maskInputFn:m,slimDOM:f=!1,dataURLOptions:g,preserveWhiteSpace:h,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:v,onStylesheetLoad:S,stylesheetLoadTimeout:b,keepIframeSrcFn:M=()=>!1}=t||{};return de(e,{doc:e,mirror:r,blockClass:n,blockSelector:i,maskTextClass:o,maskTextSelector:a,skipChild:!1,inlineStylesheet:l,maskInputOptions:u===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:u===!1?{password:!0}:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:f==="all",headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0}:f===!1?{}:f,dataURLOptions:g,inlineImages:s,recordCanvas:c,preserveWhiteSpace:h,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:v,onStylesheetLoad:S,stylesheetLoadTimeout:b,keepIframeSrcFn:M,newlyAddedElement:!1})}function W(e,t,r=document){const n={capture:!0,passive:!0};return r.addEventListener(e,t,n),()=>r.removeEventListener(e,t,n)}const fe=`Please stop import mirror directly. Instead of that,\r now you can use replayer.getMirror() to access the mirror instance of a replayer,\r or you can use record.mirror to access the mirror instance during recording.`;let wt={map:{},getId(){return console.error(fe),-1},getNode(){return console.error(fe),null},removeNodeFromMap(){console.error(fe)},has(){return console.error(fe),!1},reset(){console.error(fe)}};typeof window<"u"&&window.Proxy&&window.Reflect&&(wt=new Proxy(wt,{get(e,t,r){return t==="map"&&console.error(fe),Reflect.get(e,t,r)}}));function ve(e,t,r={}){let n=null,i=0;return function(...o){const a=Date.now();!i&&r.leading===!1&&(i=a);const l=t-(a-i),s=this;l<=0||l>t?(n&&(clearTimeout(n),n=null),i=a,e.apply(s,o)):!n&&r.trailing!==!1&&(n=setTimeout(()=>{i=r.leading===!1?0:Date.now(),n=null,e.apply(s,o)},l))}}function ke(e,t,r,n,i=window){const o=i.Object.getOwnPropertyDescriptor(e,t);return i.Object.defineProperty(e,t,n?r:{set(a){setTimeout(()=>{r.set.call(this,a)},0),o&&o.set&&o.set.call(this,a)}}),()=>ke(e,t,o||{},!0)}function he(e,t,r){try{if(!(t in e))return()=>{};const n=e[t],i=r(n);return typeof i=="function"&&(i.prototype=i.prototype||{},Object.defineProperties(i,{__rrweb_original__:{enumerable:!1,value:n}})),e[t]=i,()=>{e[t]=n}}catch{return()=>{}}}let Te=Date.now;/[1-9][0-9]{12}/.test(Date.now().toString())||(Te=()=>new Date().getTime());function Mt(e){var t,r,n,i,o,a;const l=e.document;return{left:l.scrollingElement?l.scrollingElement.scrollLeft:e.pageXOffset!==void 0?e.pageXOffset:l?.documentElement.scrollLeft||((r=(t=l?.body)===null||t===void 0?void 0:t.parentElement)===null||r===void 0?void 0:r.scrollLeft)||((n=l?.body)===null||n===void 0?void 0:n.scrollLeft)||0,top:l.scrollingElement?l.scrollingElement.scrollTop:e.pageYOffset!==void 0?e.pageYOffset:l?.documentElement.scrollTop||((o=(i=l?.body)===null||i===void 0?void 0:i.parentElement)===null||o===void 0?void 0:o.scrollTop)||((a=l?.body)===null||a===void 0?void 0:a.scrollTop)||0}}function It(){return window.innerHeight||document.documentElement&&document.documentElement.clientHeight||document.body&&document.body.clientHeight}function Ct(){return window.innerWidth||document.documentElement&&document.documentElement.clientWidth||document.body&&document.body.clientWidth}function Ot(e){return e?e.nodeType===e.ELEMENT_NODE?e:e.parentElement:null}function U(e,t,r,n){if(!e)return!1;const i=Ot(e);if(!i)return!1;try{if(typeof t=="string"){if(i.classList.contains(t)||n&&i.closest("."+t)!==null)return!0}else if(Ee(i,t,n))return!0}catch{}return!!(r&&(i.matches(r)||n&&i.closest(r)!==null))}function Pr(e,t){return t.getId(e)!==-1}function Xe(e,t){return t.getId(e)===Se}function _t(e,t){if(ge(e))return!1;const r=t.getId(e);return t.has(r)?e.parentNode&&e.parentNode.nodeType===e.DOCUMENT_NODE?!1:e.parentNode?_t(e.parentNode,t):!0:!0}function Ke(e){return!!e.changedTouches}function Fr(e=window){"NodeList"in e&&!e.NodeList.prototype.forEach&&(e.NodeList.prototype.forEach=Array.prototype.forEach),"DOMTokenList"in e&&!e.DOMTokenList.prototype.forEach&&(e.DOMTokenList.prototype.forEach=Array.prototype.forEach),Node.prototype.contains||(Node.prototype.contains=(...t)=>{let r=t[0];if(!(0 in t))throw new TypeError("1 argument is required");do if(this===r)return!0;while(r=r&&r.parentNode);return!1})}function xt(e,t){return!!(e.nodeName==="IFRAME"&&t.getMeta(e))}function Et(e,t){return!!(e.nodeName==="LINK"&&e.nodeType===e.ELEMENT_NODE&&e.getAttribute&&e.getAttribute("rel")==="stylesheet"&&t.getMeta(e))}function Ye(e){return!!e?.shadowRoot}class Br{constructor(){this.id=1,this.styleIDMap=new WeakMap,this.idStyleMap=new Map}getId(t){var r;return(r=this.styleIDMap.get(t))!==null&&r!==void 0?r:-1}has(t){return this.styleIDMap.has(t)}add(t,r){if(this.has(t))return this.getId(t);let n;return r===void 0?n=this.id++:n=r,this.styleIDMap.set(t,n),this.idStyleMap.set(n,t),n}getStyle(t){return this.idStyleMap.get(t)||null}reset(){this.styleIDMap=new WeakMap,this.idStyleMap=new Map,this.id=1}generateId(){return this.id++}}function kt(e){var t,r;let n=null;return((r=(t=e.getRootNode)===null||t===void 0?void 0:t.call(e))===null||r===void 0?void 0:r.nodeType)===Node.DOCUMENT_FRAGMENT_NODE&&e.getRootNode().host&&(n=e.getRootNode().host),n}function Wr(e){let t=e,r;for(;r=kt(t);)t=r;return t}function Ur(e){const t=e.ownerDocument;if(!t)return!1;const r=Wr(e);return t.contains(r)}function Tt(e){const t=e.ownerDocument;return t?t.contains(e)||Ur(e):!1}var _=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(_||{}),C=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(C||{}),$=(e=>(e[e.MouseUp=0]="MouseUp",e[e.MouseDown=1]="MouseDown",e[e.Click=2]="Click",e[e.ContextMenu=3]="ContextMenu",e[e.DblClick=4]="DblClick",e[e.Focus=5]="Focus",e[e.Blur=6]="Blur",e[e.TouchStart=7]="TouchStart",e[e.TouchMove_Departed=8]="TouchMove_Departed",e[e.TouchEnd=9]="TouchEnd",e[e.TouchCancel=10]="TouchCancel",e))($||{}),Y=(e=>(e[e.Mouse=0]="Mouse",e[e.Pen=1]="Pen",e[e.Touch=2]="Touch",e))(Y||{}),pe=(e=>(e[e["2D"]=0]="2D",e[e.WebGL=1]="WebGL",e[e.WebGL2=2]="WebGL2",e))(pe||{});function Rt(e){return"__ln"in e}class Hr{constructor(){this.length=0,this.head=null,this.tail=null}get(t){if(t>=this.length)throw new Error("Position outside of list range");let r=this.head;for(let n=0;n`${e}@${t}`;class zr{constructor(){this.frozen=!1,this.locked=!1,this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.mapRemoves=[],this.movedMap={},this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.processMutations=t=>{t.forEach(this.processMutation),this.emit()},this.emit=()=>{if(this.frozen||this.locked)return;const t=[],r=new Set,n=new Hr,i=s=>{let c=s,u=Se;for(;u===Se;)c=c&&c.nextSibling,u=c&&this.mirror.getId(c);return u},o=s=>{if(!s.parentNode||!Tt(s)||s.parentNode.tagName==="TEXTAREA")return;const c=ge(s.parentNode)?this.mirror.getId(kt(s)):this.mirror.getId(s.parentNode),u=i(s);if(c===-1||u===-1)return n.addNode(s);const p=de(s,{doc:this.doc,mirror:this.mirror,blockClass:this.blockClass,blockSelector:this.blockSelector,maskTextClass:this.maskTextClass,maskTextSelector:this.maskTextSelector,skipChild:!0,newlyAddedElement:!0,inlineStylesheet:this.inlineStylesheet,maskInputOptions:this.maskInputOptions,maskTextFn:this.maskTextFn,maskInputFn:this.maskInputFn,slimDOMOptions:this.slimDOMOptions,dataURLOptions:this.dataURLOptions,recordCanvas:this.recordCanvas,inlineImages:this.inlineImages,onSerialize:m=>{xt(m,this.mirror)&&this.iframeManager.addIframe(m),Et(m,this.mirror)&&this.stylesheetManager.trackLinkElement(m),Ye(s)&&this.shadowDomManager.addShadowRoot(s.shadowRoot,this.doc)},onIframeLoad:(m,f)=>{this.iframeManager.attachIframe(m,f),this.shadowDomManager.observeAttachShadow(m)},onStylesheetLoad:(m,f)=>{this.stylesheetManager.attachLinkElement(m,f)}});p&&(t.push({parentId:c,nextId:u,node:p}),r.add(p.id))};for(;this.mapRemoves.length;)this.mirror.removeNodeFromMap(this.mapRemoves.shift());for(const s of this.movedSet)Dt(this.removes,s,this.mirror)&&!this.movedSet.has(s.parentNode)||o(s);for(const s of this.addedSet)!Lt(this.droppedSet,s)&&!Dt(this.removes,s,this.mirror)||Lt(this.movedSet,s)?o(s):this.droppedSet.add(s);let a=null;for(;n.length;){let s=null;if(a){const c=this.mirror.getId(a.value.parentNode),u=i(a.value);c!==-1&&u!==-1&&(s=a)}if(!s){let c=n.tail;for(;c;){const u=c;if(c=c.previous,u){const p=this.mirror.getId(u.value.parentNode);if(i(u.value)===-1)continue;if(p!==-1){s=u;break}else{const f=u.value;if(f.parentNode&&f.parentNode.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const g=f.parentNode.host;if(this.mirror.getId(g)!==-1){s=u;break}}}}}}if(!s){for(;n.head;)n.removeNode(n.head.value);break}a=s.previous,n.removeNode(s.value),o(s.value)}const l={texts:this.texts.map(s=>{const c=s.node;return c.parentNode&&c.parentNode.tagName==="TEXTAREA"&&this.genTextAreaValueMutation(c.parentNode),{id:this.mirror.getId(c),value:s.value}}).filter(s=>!r.has(s.id)).filter(s=>this.mirror.has(s.id)),attributes:this.attributes.map(s=>{const{attributes:c}=s;if(typeof c.style=="string"){const u=JSON.stringify(s.styleDiff),p=JSON.stringify(s._unchangedStyles);u.length!r.has(s.id)).filter(s=>this.mirror.has(s.id)),removes:this.removes,adds:t};!l.texts.length&&!l.attributes.length&&!l.removes.length&&!l.adds.length||(this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.movedMap={},this.mutationCb(l))},this.genTextAreaValueMutation=t=>{let r=this.attributeMap.get(t);r||(r={node:t,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(r),this.attributeMap.set(t,r)),r.attributes.value=Array.from(t.childNodes,n=>n.textContent||"").join("")},this.processMutation=t=>{if(!Xe(t.target,this.mirror))switch(t.type){case"characterData":{const r=t.target.textContent;!U(t.target,this.blockClass,this.blockSelector,!1)&&r!==t.oldValue&&this.texts.push({value:bt(t.target,this.maskTextClass,this.maskTextSelector,!0)&&r?this.maskTextFn?this.maskTextFn(r,Ot(t.target)):r.replace(/[\S]/g,"*"):r,node:t.target});break}case"attributes":{const r=t.target;let n=t.attributeName,i=t.target.getAttribute(n);if(n==="value"){const a=qe(r);i=Ve({element:r,maskInputOptions:this.maskInputOptions,tagName:r.tagName,type:a,value:i,maskInputFn:this.maskInputFn})}if(U(t.target,this.blockClass,this.blockSelector,!1)||i===t.oldValue)return;let o=this.attributeMap.get(t.target);if(r.tagName==="IFRAME"&&n==="src"&&!this.keepIframeSrcFn(i))if(!r.contentDocument)n="rr_src";else return;if(o||(o={node:t.target,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(o),this.attributeMap.set(t.target,o)),n==="type"&&r.tagName==="INPUT"&&(t.oldValue||"").toLowerCase()==="password"&&r.setAttribute("data-rr-is-password","true"),!vt(r.tagName,n)&&(o.attributes[n]=St(this.doc,te(r.tagName),te(n),i),n==="style")){if(!this.unattachedDoc)try{this.unattachedDoc=document.implementation.createHTMLDocument()}catch{this.unattachedDoc=this.doc}const a=this.unattachedDoc.createElement("span");t.oldValue&&a.setAttribute("style",t.oldValue);for(const l of Array.from(r.style)){const s=r.style.getPropertyValue(l),c=r.style.getPropertyPriority(l);s!==a.style.getPropertyValue(l)||c!==a.style.getPropertyPriority(l)?c===""?o.styleDiff[l]=s:o.styleDiff[l]=[s,c]:o._unchangedStyles[l]=[s,c]}for(const l of Array.from(a.style))r.style.getPropertyValue(l)===""&&(o.styleDiff[l]=!1)}break}case"childList":{if(U(t.target,this.blockClass,this.blockSelector,!0))return;if(t.target.tagName==="TEXTAREA"){this.genTextAreaValueMutation(t.target);return}t.addedNodes.forEach(r=>this.genAdds(r,t.target)),t.removedNodes.forEach(r=>{const n=this.mirror.getId(r),i=ge(t.target)?this.mirror.getId(t.target.host):this.mirror.getId(t.target);U(t.target,this.blockClass,this.blockSelector,!1)||Xe(r,this.mirror)||!Pr(r,this.mirror)||(this.addedSet.has(r)?(Qe(this.addedSet,r),this.droppedSet.add(r)):this.addedSet.has(t.target)&&n===-1||_t(t.target,this.mirror)||(this.movedSet.has(r)&&this.movedMap[Nt(n,i)]?Qe(this.movedSet,r):this.removes.push({parentId:i,id:n,isShadow:ge(t.target)&&ye(t.target)?!0:void 0})),this.mapRemoves.push(r))});break}}},this.genAdds=(t,r)=>{if(!this.processedNodeManager.inOtherBuffer(t,this)&&!(this.addedSet.has(t)||this.movedSet.has(t))){if(this.mirror.hasNode(t)){if(Xe(t,this.mirror))return;this.movedSet.add(t);let n=null;r&&this.mirror.hasNode(r)&&(n=this.mirror.getId(r)),n&&n!==-1&&(this.movedMap[Nt(this.mirror.getId(t),n)]=!0)}else this.addedSet.add(t),this.droppedSet.delete(t);U(t,this.blockClass,this.blockSelector,!1)||(t.childNodes.forEach(n=>this.genAdds(n)),Ye(t)&&t.shadowRoot.childNodes.forEach(n=>{this.processedNodeManager.add(n,this),this.genAdds(n,t)}))}}}init(t){["mutationCb","blockClass","blockSelector","maskTextClass","maskTextSelector","inlineStylesheet","maskInputOptions","maskTextFn","maskInputFn","keepIframeSrcFn","recordCanvas","inlineImages","slimDOMOptions","dataURLOptions","doc","mirror","iframeManager","stylesheetManager","shadowDomManager","canvasManager","processedNodeManager"].forEach(r=>{this[r]=t[r]})}freeze(){this.frozen=!0,this.canvasManager.freeze()}unfreeze(){this.frozen=!1,this.canvasManager.unfreeze(),this.emit()}isFrozen(){return this.frozen}lock(){this.locked=!0,this.canvasManager.lock()}unlock(){this.locked=!1,this.canvasManager.unlock(),this.emit()}reset(){this.shadowDomManager.reset(),this.canvasManager.reset()}}function Qe(e,t){e.delete(t),t.childNodes.forEach(r=>Qe(e,r))}function Dt(e,t,r){return e.length===0?!1:At(e,t,r)}function At(e,t,r){const{parentNode:n}=t;if(!n)return!1;const i=r.getId(n);return e.some(o=>o.id===i)?!0:At(e,n,r)}function Lt(e,t){return e.size===0?!1:Pt(e,t)}function Pt(e,t){const{parentNode:r}=t;return r?e.has(r)?!0:Pt(e,r):!1}let be;function $r(e){be=e}function Gr(){be=void 0}const O=e=>be?(...r)=>{try{return e(...r)}catch(n){if(be&&be(n)===!0)return;throw n}}:e,re=[];function we(e){try{if("composedPath"in e){const t=e.composedPath();if(t.length)return t[0]}else if("path"in e&&e.path.length)return e.path[0]}catch{}return e&&e.target}function Ft(e,t){var r,n;const i=new zr;re.push(i),i.init(e);let o=window.MutationObserver||window.__rrMutationObserver;const a=(n=(r=window?.Zone)===null||r===void 0?void 0:r.__symbol__)===null||n===void 0?void 0:n.call(r,"MutationObserver");a&&window[a]&&(o=window[a]);const l=new o(O(i.processMutations.bind(i)));return l.observe(t,{attributes:!0,attributeOldValue:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0}),l}function jr({mousemoveCb:e,sampling:t,doc:r,mirror:n}){if(t.mousemove===!1)return()=>{};const i=typeof t.mousemove=="number"?t.mousemove:50,o=typeof t.mousemoveCallback=="number"?t.mousemoveCallback:500;let a=[],l;const s=ve(O(p=>{const m=Date.now()-l;e(a.map(f=>(f.timeOffset-=m,f)),p),a=[],l=null}),o),c=O(ve(O(p=>{const m=we(p),{clientX:f,clientY:g}=Ke(p)?p.changedTouches[0]:p;l||(l=Te()),a.push({x:f,y:g,id:n.getId(m),timeOffset:Te()-l}),s(typeof DragEvent<"u"&&p instanceof DragEvent?C.Drag:p instanceof MouseEvent?C.MouseMove:C.TouchMove)}),i,{trailing:!1})),u=[W("mousemove",c,r),W("touchmove",c,r),W("drag",c,r)];return O(()=>{u.forEach(p=>p())})}function Vr({mouseInteractionCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){if(o.mouseInteraction===!1)return()=>{};const a=o.mouseInteraction===!0||o.mouseInteraction===void 0?{}:o.mouseInteraction,l=[];let s=null;const c=u=>p=>{const m=we(p);if(U(m,n,i,!0))return;let f=null,g=u;if("pointerType"in p){switch(p.pointerType){case"mouse":f=Y.Mouse;break;case"touch":f=Y.Touch;break;case"pen":f=Y.Pen;break}f===Y.Touch?$[u]===$.MouseDown?g="TouchStart":$[u]===$.MouseUp&&(g="TouchEnd"):Y.Pen}else Ke(p)&&(f=Y.Touch);f!==null?(s=f,(g.startsWith("Touch")&&f===Y.Touch||g.startsWith("Mouse")&&f===Y.Mouse)&&(f=null)):$[u]===$.Click&&(f=s,s=null);const h=Ke(p)?p.changedTouches[0]:p;if(!h)return;const y=r.getId(m),{clientX:w,clientY:v}=h;O(e)(Object.assign({type:$[g],id:y,x:w,y:v},f!==null&&{pointerType:f}))};return Object.keys($).filter(u=>Number.isNaN(Number(u))&&!u.endsWith("_Departed")&&a[u]!==!1).forEach(u=>{let p=te(u);const m=c(u);if(window.PointerEvent)switch($[u]){case $.MouseDown:case $.MouseUp:p=p.replace("mouse","pointer");break;case $.TouchStart:case $.TouchEnd:return}l.push(W(p,m,t))}),O(()=>{l.forEach(u=>u())})}function Bt({scrollCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){const a=O(ve(O(l=>{const s=we(l);if(!s||U(s,n,i,!0))return;const c=r.getId(s);if(s===t&&t.defaultView){const u=Mt(t.defaultView);e({id:c,x:u.left,y:u.top})}else e({id:c,x:s.scrollLeft,y:s.scrollTop})}),o.scroll||100));return W("scroll",a,t)}function qr({viewportResizeCb:e},{win:t}){let r=-1,n=-1;const i=O(ve(O(()=>{const o=It(),a=Ct();(r!==o||n!==a)&&(e({width:Number(a),height:Number(o)}),r=o,n=a)}),200));return W("resize",i,t)}const Jr=["INPUT","TEXTAREA","SELECT"],Wt=new WeakMap;function Xr({inputCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,ignoreClass:o,ignoreSelector:a,maskInputOptions:l,maskInputFn:s,sampling:c,userTriggeredOnInput:u}){function p(v){let S=we(v);const b=v.isTrusted,M=S&&S.tagName;if(S&&M==="OPTION"&&(S=S.parentElement),!S||!M||Jr.indexOf(M)<0||U(S,n,i,!0)||S.classList.contains(o)||a&&S.matches(a))return;let F=S.value,P=!1;const k=qe(S)||"";k==="radio"||k==="checkbox"?P=S.checked:(l[M.toLowerCase()]||l[k])&&(F=Ve({element:S,maskInputOptions:l,tagName:M,type:k,value:F,maskInputFn:s})),m(S,u?{text:F,isChecked:P,userTriggered:b}:{text:F,isChecked:P});const T=S.name;k==="radio"&&T&&P&&t.querySelectorAll(`input[type="radio"][name="${T}"]`).forEach(j=>{if(j!==S){const V=j.value;m(j,u?{text:V,isChecked:!P,userTriggered:!1}:{text:V,isChecked:!P})}})}function m(v,S){const b=Wt.get(v);if(!b||b.text!==S.text||b.isChecked!==S.isChecked){Wt.set(v,S);const M=r.getId(v);O(e)(Object.assign(Object.assign({},S),{id:M}))}}const g=(c.input==="last"?["change"]:["input","change"]).map(v=>W(v,O(p),t)),h=t.defaultView;if(!h)return()=>{g.forEach(v=>v())};const y=h.Object.getOwnPropertyDescriptor(h.HTMLInputElement.prototype,"value"),w=[[h.HTMLInputElement.prototype,"value"],[h.HTMLInputElement.prototype,"checked"],[h.HTMLSelectElement.prototype,"value"],[h.HTMLTextAreaElement.prototype,"value"],[h.HTMLSelectElement.prototype,"selectedIndex"],[h.HTMLOptionElement.prototype,"selected"]];return y&&y.set&&g.push(...w.map(v=>ke(v[0],v[1],{set(){O(p)({target:this,isTrusted:!1})}},!1,h))),O(()=>{g.forEach(v=>v())})}function Re(e){const t=[];function r(n,i){if(Ne("CSSGroupingRule")&&n.parentRule instanceof CSSGroupingRule||Ne("CSSMediaRule")&&n.parentRule instanceof CSSMediaRule||Ne("CSSSupportsRule")&&n.parentRule instanceof CSSSupportsRule||Ne("CSSConditionRule")&&n.parentRule instanceof CSSConditionRule){const a=Array.from(n.parentRule.cssRules).indexOf(n);i.unshift(a)}else if(n.parentStyleSheet){const a=Array.from(n.parentStyleSheet.cssRules).indexOf(n);i.unshift(a)}return i}return r(e,t)}function Z(e,t,r){let n,i;return e?(e.ownerNode?n=t.getId(e.ownerNode):i=r.getId(e),{styleId:i,id:n}):{}}function Kr({styleSheetRuleCb:e,mirror:t,stylesheetManager:r},{win:n}){if(!n.CSSStyleSheet||!n.CSSStyleSheet.prototype)return()=>{};const i=n.CSSStyleSheet.prototype.insertRule;n.CSSStyleSheet.prototype.insertRule=new Proxy(i,{apply:O((u,p,m)=>{const[f,g]=m,{id:h,styleId:y}=Z(p,t,r.styleMirror);return(h&&h!==-1||y&&y!==-1)&&e({id:h,styleId:y,adds:[{rule:f,index:g}]}),u.apply(p,m)})});const o=n.CSSStyleSheet.prototype.deleteRule;n.CSSStyleSheet.prototype.deleteRule=new Proxy(o,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,removes:[{index:f}]}),u.apply(p,m)})});let a;n.CSSStyleSheet.prototype.replace&&(a=n.CSSStyleSheet.prototype.replace,n.CSSStyleSheet.prototype.replace=new Proxy(a,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,replace:f}),u.apply(p,m)})}));let l;n.CSSStyleSheet.prototype.replaceSync&&(l=n.CSSStyleSheet.prototype.replaceSync,n.CSSStyleSheet.prototype.replaceSync=new Proxy(l,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,replaceSync:f}),u.apply(p,m)})}));const s={};De("CSSGroupingRule")?s.CSSGroupingRule=n.CSSGroupingRule:(De("CSSMediaRule")&&(s.CSSMediaRule=n.CSSMediaRule),De("CSSConditionRule")&&(s.CSSConditionRule=n.CSSConditionRule),De("CSSSupportsRule")&&(s.CSSSupportsRule=n.CSSSupportsRule));const c={};return Object.entries(s).forEach(([u,p])=>{c[u]={insertRule:p.prototype.insertRule,deleteRule:p.prototype.deleteRule},p.prototype.insertRule=new Proxy(c[u].insertRule,{apply:O((m,f,g)=>{const[h,y]=g,{id:w,styleId:v}=Z(f.parentStyleSheet,t,r.styleMirror);return(w&&w!==-1||v&&v!==-1)&&e({id:w,styleId:v,adds:[{rule:h,index:[...Re(f),y||0]}]}),m.apply(f,g)})}),p.prototype.deleteRule=new Proxy(c[u].deleteRule,{apply:O((m,f,g)=>{const[h]=g,{id:y,styleId:w}=Z(f.parentStyleSheet,t,r.styleMirror);return(y&&y!==-1||w&&w!==-1)&&e({id:y,styleId:w,removes:[{index:[...Re(f),h]}]}),m.apply(f,g)})})}),O(()=>{n.CSSStyleSheet.prototype.insertRule=i,n.CSSStyleSheet.prototype.deleteRule=o,a&&(n.CSSStyleSheet.prototype.replace=a),l&&(n.CSSStyleSheet.prototype.replaceSync=l),Object.entries(s).forEach(([u,p])=>{p.prototype.insertRule=c[u].insertRule,p.prototype.deleteRule=c[u].deleteRule})})}function Ut({mirror:e,stylesheetManager:t},r){var n,i,o;let a=null;r.nodeName==="#document"?a=e.getId(r):a=e.getId(r.host);const l=r.nodeName==="#document"?(n=r.defaultView)===null||n===void 0?void 0:n.Document:(o=(i=r.ownerDocument)===null||i===void 0?void 0:i.defaultView)===null||o===void 0?void 0:o.ShadowRoot,s=l?.prototype?Object.getOwnPropertyDescriptor(l?.prototype,"adoptedStyleSheets"):void 0;return a===null||a===-1||!l||!s?()=>{}:(Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get(){var c;return(c=s.get)===null||c===void 0?void 0:c.call(this)},set(c){var u;const p=(u=s.set)===null||u===void 0?void 0:u.call(this,c);if(a!==null&&a!==-1)try{t.adoptStyleSheets(c,a)}catch{}return p}}),O(()=>{Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get:s.get,set:s.set})}))}function Yr({styleDeclarationCb:e,mirror:t,ignoreCSSAttributes:r,stylesheetManager:n},{win:i}){const o=i.CSSStyleDeclaration.prototype.setProperty;i.CSSStyleDeclaration.prototype.setProperty=new Proxy(o,{apply:O((l,s,c)=>{var u;const[p,m,f]=c;if(r.has(p))return o.apply(s,[p,m,f]);const{id:g,styleId:h}=Z((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,set:{property:p,value:m,priority:f},index:Re(s.parentRule)}),l.apply(s,c)})});const a=i.CSSStyleDeclaration.prototype.removeProperty;return i.CSSStyleDeclaration.prototype.removeProperty=new Proxy(a,{apply:O((l,s,c)=>{var u;const[p]=c;if(r.has(p))return a.apply(s,[p]);const{id:m,styleId:f}=Z((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(m&&m!==-1||f&&f!==-1)&&e({id:m,styleId:f,remove:{property:p},index:Re(s.parentRule)}),l.apply(s,c)})}),O(()=>{i.CSSStyleDeclaration.prototype.setProperty=o,i.CSSStyleDeclaration.prototype.removeProperty=a})}function Qr({mediaInteractionCb:e,blockClass:t,blockSelector:r,mirror:n,sampling:i,doc:o}){const a=O(s=>ve(O(c=>{const u=we(c);if(!u||U(u,t,r,!0))return;const{currentTime:p,volume:m,muted:f,playbackRate:g,loop:h}=u;e({type:s,id:n.getId(u),currentTime:p,volume:m,muted:f,playbackRate:g,loop:h})}),i.media||500)),l=[W("play",a(0),o),W("pause",a(1),o),W("seeked",a(2),o),W("volumechange",a(3),o),W("ratechange",a(4),o)];return O(()=>{l.forEach(s=>s())})}function Zr({fontCb:e,doc:t}){const r=t.defaultView;if(!r)return()=>{};const n=[],i=new WeakMap,o=r.FontFace;r.FontFace=function(s,c,u){const p=new o(s,c,u);return i.set(p,{family:s,buffer:typeof c!="string",descriptors:u,fontSource:typeof c=="string"?c:JSON.stringify(Array.from(new Uint8Array(c)))}),p};const a=he(t.fonts,"add",function(l){return function(s){return setTimeout(O(()=>{const c=i.get(s);c&&(e(c),i.delete(s))}),0),l.apply(this,[s])}});return n.push(()=>{r.FontFace=o}),n.push(a),O(()=>{n.forEach(l=>l())})}function en(e){const{doc:t,mirror:r,blockClass:n,blockSelector:i,selectionCb:o}=e;let a=!0;const l=O(()=>{const s=t.getSelection();if(!s||a&&s?.isCollapsed)return;a=s.isCollapsed||!1;const c=[],u=s.rangeCount||0;for(let p=0;p{}:he(r.customElements,"define",function(i){return function(o,a,l){try{t({define:{name:o}})}catch{console.warn(`Custom element callback failed for ${o}`)}return i.apply(this,[o,a,l])}})}function rn(e,t){const{mutationCb:r,mousemoveCb:n,mouseInteractionCb:i,scrollCb:o,viewportResizeCb:a,inputCb:l,mediaInteractionCb:s,styleSheetRuleCb:c,styleDeclarationCb:u,canvasMutationCb:p,fontCb:m,selectionCb:f,customElementCb:g}=e;e.mutationCb=(...h)=>{t.mutation&&t.mutation(...h),r(...h)},e.mousemoveCb=(...h)=>{t.mousemove&&t.mousemove(...h),n(...h)},e.mouseInteractionCb=(...h)=>{t.mouseInteraction&&t.mouseInteraction(...h),i(...h)},e.scrollCb=(...h)=>{t.scroll&&t.scroll(...h),o(...h)},e.viewportResizeCb=(...h)=>{t.viewportResize&&t.viewportResize(...h),a(...h)},e.inputCb=(...h)=>{t.input&&t.input(...h),l(...h)},e.mediaInteractionCb=(...h)=>{t.mediaInteaction&&t.mediaInteaction(...h),s(...h)},e.styleSheetRuleCb=(...h)=>{t.styleSheetRule&&t.styleSheetRule(...h),c(...h)},e.styleDeclarationCb=(...h)=>{t.styleDeclaration&&t.styleDeclaration(...h),u(...h)},e.canvasMutationCb=(...h)=>{t.canvasMutation&&t.canvasMutation(...h),p(...h)},e.fontCb=(...h)=>{t.font&&t.font(...h),m(...h)},e.selectionCb=(...h)=>{t.selection&&t.selection(...h),f(...h)},e.customElementCb=(...h)=>{t.customElement&&t.customElement(...h),g(...h)}}function nn(e,t={}){const r=e.doc.defaultView;if(!r)return()=>{};rn(e,t);let n;e.recordDOM&&(n=Ft(e,e.doc));const i=jr(e),o=Vr(e),a=Bt(e),l=qr(e,{win:r}),s=Xr(e),c=Qr(e);let u=()=>{},p=()=>{},m=()=>{},f=()=>{};e.recordDOM&&(u=Kr(e,{win:r}),p=Ut(e,e.doc),m=Yr(e,{win:r}),e.collectFonts&&(f=Zr(e)));const g=en(e),h=tn(e),y=[];for(const w of e.plugins)y.push(w.observer(w.callback,r,w.options));return O(()=>{re.forEach(w=>w.reset()),n?.disconnect(),i(),o(),a(),l(),s(),c(),u(),p(),m(),f(),g(),h(),y.forEach(w=>w())})}function Ne(e){return typeof window[e]<"u"}function De(e){return!!(typeof window[e]<"u"&&window[e].prototype&&"insertRule"in window[e].prototype&&"deleteRule"in window[e].prototype)}class Ht{constructor(t){this.generateIdFn=t,this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap}getId(t,r,n,i){const o=n||this.getIdToRemoteIdMap(t),a=i||this.getRemoteIdToIdMap(t);let l=o.get(r);return l||(l=this.generateIdFn(),o.set(r,l),a.set(l,r)),l}getIds(t,r){const n=this.getIdToRemoteIdMap(t),i=this.getRemoteIdToIdMap(t);return r.map(o=>this.getId(t,o,n,i))}getRemoteId(t,r,n){const i=n||this.getRemoteIdToIdMap(t);if(typeof r!="number")return r;const o=i.get(r);return o||-1}getRemoteIds(t,r){const n=this.getRemoteIdToIdMap(t);return r.map(i=>this.getRemoteId(t,i,n))}reset(t){if(!t){this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap;return}this.iframeIdToRemoteIdMap.delete(t),this.iframeRemoteIdToIdMap.delete(t)}getIdToRemoteIdMap(t){let r=this.iframeIdToRemoteIdMap.get(t);return r||(r=new Map,this.iframeIdToRemoteIdMap.set(t,r)),r}getRemoteIdToIdMap(t){let r=this.iframeRemoteIdToIdMap.get(t);return r||(r=new Map,this.iframeRemoteIdToIdMap.set(t,r)),r}}class on{constructor(t){this.iframes=new WeakMap,this.crossOriginIframeMap=new WeakMap,this.crossOriginIframeMirror=new Ht(gt),this.crossOriginIframeRootIdMap=new WeakMap,this.mutationCb=t.mutationCb,this.wrappedEmit=t.wrappedEmit,this.stylesheetManager=t.stylesheetManager,this.recordCrossOriginIframes=t.recordCrossOriginIframes,this.crossOriginIframeStyleMirror=new Ht(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)),this.mirror=t.mirror,this.recordCrossOriginIframes&&window.addEventListener("message",this.handleMessage.bind(this))}addIframe(t){this.iframes.set(t,!0),t.contentWindow&&this.crossOriginIframeMap.set(t.contentWindow,t)}addLoadListener(t){this.loadListener=t}attachIframe(t,r){var n;this.mutationCb({adds:[{parentId:this.mirror.getId(t),nextId:null,node:r}],removes:[],texts:[],attributes:[],isAttachIframe:!0}),(n=this.loadListener)===null||n===void 0||n.call(this,t),t.contentDocument&&t.contentDocument.adoptedStyleSheets&&t.contentDocument.adoptedStyleSheets.length>0&&this.stylesheetManager.adoptStyleSheets(t.contentDocument.adoptedStyleSheets,this.mirror.getId(t.contentDocument))}handleMessage(t){const r=t;if(r.data.type!=="rrweb"||r.origin!==r.data.origin||!t.source)return;const i=this.crossOriginIframeMap.get(t.source);if(!i)return;const o=this.transformCrossOriginEvent(i,r.data.event);o&&this.wrappedEmit(o,r.data.isCheckout)}transformCrossOriginEvent(t,r){var n;switch(r.type){case _.FullSnapshot:{this.crossOriginIframeMirror.reset(t),this.crossOriginIframeStyleMirror.reset(t),this.replaceIdOnNode(r.data.node,t);const i=r.data.node.id;return this.crossOriginIframeRootIdMap.set(t,i),this.patchRootIdOnNode(r.data.node,i),{timestamp:r.timestamp,type:_.IncrementalSnapshot,data:{source:C.Mutation,adds:[{parentId:this.mirror.getId(t),nextId:null,node:r.data.node}],removes:[],texts:[],attributes:[],isAttachIframe:!0}}}case _.Meta:case _.Load:case _.DomContentLoaded:return!1;case _.Plugin:return r;case _.Custom:return this.replaceIds(r.data.payload,t,["id","parentId","previousId","nextId"]),r;case _.IncrementalSnapshot:switch(r.data.source){case C.Mutation:return r.data.adds.forEach(i=>{this.replaceIds(i,t,["parentId","nextId","previousId"]),this.replaceIdOnNode(i.node,t);const o=this.crossOriginIframeRootIdMap.get(t);o&&this.patchRootIdOnNode(i.node,o)}),r.data.removes.forEach(i=>{this.replaceIds(i,t,["parentId","id"])}),r.data.attributes.forEach(i=>{this.replaceIds(i,t,["id"])}),r.data.texts.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.Drag:case C.TouchMove:case C.MouseMove:return r.data.positions.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.ViewportResize:return!1;case C.MediaInteraction:case C.MouseInteraction:case C.Scroll:case C.CanvasMutation:case C.Input:return this.replaceIds(r.data,t,["id"]),r;case C.StyleSheetRule:case C.StyleDeclaration:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleId"]),r;case C.Font:return r;case C.Selection:return r.data.ranges.forEach(i=>{this.replaceIds(i,t,["start","end"])}),r;case C.AdoptedStyleSheet:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleIds"]),(n=r.data.styles)===null||n===void 0||n.forEach(i=>{this.replaceStyleIds(i,t,["styleId"])}),r}}return!1}replace(t,r,n,i){for(const o of i)!Array.isArray(r[o])&&typeof r[o]!="number"||(Array.isArray(r[o])?r[o]=t.getIds(n,r[o]):r[o]=t.getId(n,r[o]));return r}replaceIds(t,r,n){return this.replace(this.crossOriginIframeMirror,t,r,n)}replaceStyleIds(t,r,n){return this.replace(this.crossOriginIframeStyleMirror,t,r,n)}replaceIdOnNode(t,r){this.replaceIds(t,r,["id","rootId"]),"childNodes"in t&&t.childNodes.forEach(n=>{this.replaceIdOnNode(n,r)})}patchRootIdOnNode(t,r){t.type!==A.Document&&!t.rootId&&(t.rootId=r),"childNodes"in t&&t.childNodes.forEach(n=>{this.patchRootIdOnNode(n,r)})}}class sn{constructor(t){this.shadowDoms=new WeakSet,this.restoreHandlers=[],this.mutationCb=t.mutationCb,this.scrollCb=t.scrollCb,this.bypassOptions=t.bypassOptions,this.mirror=t.mirror,this.init()}init(){this.reset(),this.patchAttachShadow(Element,document)}addShadowRoot(t,r){if(!ye(t)||this.shadowDoms.has(t))return;this.shadowDoms.add(t);const n=Ft(Object.assign(Object.assign({},this.bypassOptions),{doc:r,mutationCb:this.mutationCb,mirror:this.mirror,shadowDomManager:this}),t);this.restoreHandlers.push(()=>n.disconnect()),this.restoreHandlers.push(Bt(Object.assign(Object.assign({},this.bypassOptions),{scrollCb:this.scrollCb,doc:t,mirror:this.mirror}))),setTimeout(()=>{t.adoptedStyleSheets&&t.adoptedStyleSheets.length>0&&this.bypassOptions.stylesheetManager.adoptStyleSheets(t.adoptedStyleSheets,this.mirror.getId(t.host)),this.restoreHandlers.push(Ut({mirror:this.mirror,stylesheetManager:this.bypassOptions.stylesheetManager},t))},0)}observeAttachShadow(t){!t.contentWindow||!t.contentDocument||this.patchAttachShadow(t.contentWindow.Element,t.contentDocument)}patchAttachShadow(t,r){const n=this;this.restoreHandlers.push(he(t.prototype,"attachShadow",function(i){return function(o){const a=i.call(this,o);return this.shadowRoot&&Tt(this)&&n.addShadowRoot(this.shadowRoot,r),a}}))}reset(){this.restoreHandlers.forEach(t=>{try{t()}catch{}}),this.restoreHandlers=[],this.shadowDoms=new WeakSet}}/*! ***************************************************************************** +======= +(function(){"use strict";var A;(function(e){e[e.Document=0]="Document",e[e.DocumentType=1]="DocumentType",e[e.Element=2]="Element",e[e.Text=3]="Text",e[e.CDATA=4]="CDATA",e[e.Comment=5]="Comment"})(A||(A={}));function gr(e){return e.nodeType===e.ELEMENT_NODE}function be(e){const t=e?.host;return t?.shadowRoot===e}function we(e){return Object.prototype.toString.call(e)==="[object ShadowRoot]"}function yr(e){return e.includes(" background-clip: text;")&&!e.includes(" -webkit-background-clip: text;")&&(e=e.replace(" background-clip: text;"," -webkit-background-clip: text; background-clip: text;")),e}function vr(e){const{cssText:t}=e;if(t.split('"').length<3)return t;const r=["@import",`url(${JSON.stringify(e.href)})`];return e.layerName===""?r.push("layer"):e.layerName&&r.push(`layer(${e.layerName})`),e.supportsText&&r.push(`supports(${e.supportsText})`),e.media.length&&r.push(e.media.mediaText),r.join(" ")+";"}function Te(e){try{const t=e.rules||e.cssRules;return t?yr(Array.from(t,vt).join("")):null}catch{return null}}function vt(e){let t;if(br(e))try{t=Te(e.styleSheet)||vr(e)}catch{}else if(wr(e)&&e.selectorText.includes(":"))return Sr(e.cssText);return t||e.cssText}function Sr(e){const t=/(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;return e.replace(t,"$1\\$2")}function br(e){return"styleSheet"in e}function wr(e){return"selectorText"in e}class St{constructor(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}getId(t){var r;if(!t)return-1;const n=(r=this.getMeta(t))===null||r===void 0?void 0:r.id;return n??-1}getNode(t){return this.idNodeMap.get(t)||null}getIds(){return Array.from(this.idNodeMap.keys())}getMeta(t){return this.nodeMetaMap.get(t)||null}removeNodeFromMap(t){const r=this.getId(t);this.idNodeMap.delete(r),t.childNodes&&t.childNodes.forEach(n=>this.removeNodeFromMap(n))}has(t){return this.idNodeMap.has(t)}hasNode(t){return this.nodeMetaMap.has(t)}add(t,r){const n=r.id;this.idNodeMap.set(n,t),this.nodeMetaMap.set(t,r)}replace(t,r){const n=this.getNode(t);if(n){const i=this.nodeMetaMap.get(n);i&&this.nodeMetaMap.set(r,i)}this.idNodeMap.set(t,r)}reset(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}}function Ir(){return new St}function et({element:e,maskInputOptions:t,tagName:r,type:n,value:i,maskInputFn:o}){let a=i||"";const l=n&&ie(n);return(t[r.toLowerCase()]||l&&t[l])&&(o?a=o(a,e):a="*".repeat(a.length)),a}function ie(e){return e.toLowerCase()}const bt="__rrweb_original__";function Mr(e){const t=e.getContext("2d");if(!t)return!0;const r=50;for(let n=0;ns!==0))return!1}return!0}function tt(e){const t=e.type;return e.hasAttribute("data-rr-is-password")?"password":t?ie(t):null}function wt(e,t){var r;let n;try{n=new URL(e,t??window.location.href)}catch{return null}const i=/\.([0-9a-z]+)(?:$)/i,o=n.pathname.match(i);return(r=o?.[1])!==null&&r!==void 0?r:null}let Cr=1;const _r=new RegExp("[^a-z0-9-_:]"),Ie=-2;function It(){return Cr++}function Er(e){if(e instanceof HTMLFormElement)return"form";const t=ie(e.tagName);return _r.test(t)?"div":t}function Or(e){let t="";return e.indexOf("//")>-1?t=e.split("/").slice(0,3).join("/"):t=e.split("/")[0],t=t.split("?")[0],t}let fe,Mt;const xr=/url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm,kr=/^(?:[a-z+]+:)?\/\//i,Tr=/^www\..*/i,Rr=/^(data:)([^,]*),(.*)/i;function Re(e,t){return(e||"").replace(xr,(r,n,i,o,a,l)=>{const s=i||a||l,u=n||o||"";if(!s)return r;if(kr.test(s)||Tr.test(s))return`url(${u}${s}${u})`;if(Rr.test(s))return`url(${u}${s}${u})`;if(s[0]==="/")return`url(${u}${Or(t)+s}${u})`;const c=t.split("/"),h=s.split("/");c.pop();for(const m of h)m!=="."&&(m===".."?c.pop():c.push(m));return`url(${u}${c.join("/")}${u})`})}const Dr=/^[^ \t\n\r\u000c]+/,Nr=/^[, \t\n\r\u000c]+/;function Ar(e,t){if(t.trim()==="")return t;let r=0;function n(o){let a;const l=o.exec(t.substring(r));return l?(a=l[0],r+=a.length,a):""}const i=[];for(;n(Nr),!(r>=t.length);){let o=n(Dr);if(o.slice(-1)===",")o=he(e,o.substring(0,o.length-1)),i.push(o);else{let a="";o=he(e,o);let l=!1;for(;;){const s=t.charAt(r);if(s===""){i.push((o+a).trim());break}else if(l)s===")"&&(l=!1);else if(s===","){r+=1,i.push((o+a).trim());break}else s==="("&&(l=!0);a+=s,r+=1}}}return i.join(", ")}function he(e,t){if(!t||t.trim()==="")return t;const r=e.createElement("a");return r.href=t,r.href}function Lr(e){return!!(e.tagName==="svg"||e.ownerSVGElement)}function rt(){const e=document.createElement("a");return e.href="",e.href}function Ct(e,t,r,n){return n&&(r==="src"||r==="href"&&!(t==="use"&&n[0]==="#")||r==="xlink:href"&&n[0]!=="#"||r==="background"&&(t==="table"||t==="td"||t==="th")?he(e,n):r==="srcset"?Ar(e,n):r==="style"?Re(n,rt()):t==="object"&&r==="data"?he(e,n):n)}function _t(e,t,r){return(e==="video"||e==="audio")&&t==="autoplay"}function Fr(e,t,r){try{if(typeof t=="string"){if(e.classList.contains(t))return!0}else for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}if(r)return e.matches(r)}catch{}return!1}function De(e,t,r){if(!e)return!1;if(e.nodeType!==e.ELEMENT_NODE)return r?De(e.parentNode,t,r):!1;for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}return r?De(e.parentNode,t,r):!1}function Et(e,t,r,n){try{const i=e.nodeType===e.ELEMENT_NODE?e:e.parentElement;if(i===null)return!1;if(typeof t=="string"){if(n){if(i.closest(`.${t}`))return!0}else if(i.classList.contains(t))return!0}else if(De(i,t,n))return!0;if(r){if(n){if(i.closest(r))return!0}else if(i.matches(r))return!0}}catch{}return!1}function Pr(e,t,r){const n=e.contentWindow;if(!n)return;let i=!1,o;try{o=n.document.readyState}catch{return}if(o!=="complete"){const l=setTimeout(()=>{i||(t(),i=!0)},r);e.addEventListener("load",()=>{clearTimeout(l),i=!0,t()});return}const a="about:blank";if(n.location.href!==a||e.src===a||e.src==="")return setTimeout(t,0),e.addEventListener("load",t);e.addEventListener("load",t)}function Br(e,t,r){let n=!1,i;try{i=e.sheet}catch{return}if(i)return;const o=setTimeout(()=>{n||(t(),n=!0)},r);e.addEventListener("load",()=>{clearTimeout(o),n=!0,t()})}function Wr(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:a,inlineStylesheet:l,maskInputOptions:s={},maskTextFn:u,maskInputFn:c,dataURLOptions:h={},inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:p=!1}=t,y=Ur(r,n);switch(e.nodeType){case e.DOCUMENT_NODE:return e.compatMode!=="CSS1Compat"?{type:A.Document,childNodes:[],compatMode:e.compatMode}:{type:A.Document,childNodes:[]};case e.DOCUMENT_TYPE_NODE:return{type:A.DocumentType,name:e.name,publicId:e.publicId,systemId:e.systemId,rootId:y};case e.ELEMENT_NODE:return zr(e,{doc:r,blockClass:i,blockSelector:o,inlineStylesheet:l,maskInputOptions:s,maskInputFn:c,dataURLOptions:h,inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:p,rootId:y});case e.TEXT_NODE:return Hr(e,{needsMask:a,maskTextFn:u,rootId:y});case e.CDATA_SECTION_NODE:return{type:A.CDATA,textContent:"",rootId:y};case e.COMMENT_NODE:return{type:A.Comment,textContent:e.textContent||"",rootId:y};default:return!1}}function Ur(e,t){if(!t.hasNode(e))return;const r=t.getId(e);return r===1?void 0:r}function Hr(e,t){var r;const{needsMask:n,maskTextFn:i,rootId:o}=t,a=e.parentNode&&e.parentNode.tagName;let l=e.textContent;const s=a==="STYLE"?!0:void 0,u=a==="SCRIPT"?!0:void 0;if(s&&l){try{e.nextSibling||e.previousSibling||!((r=e.parentNode.sheet)===null||r===void 0)&&r.cssRules&&(l=Te(e.parentNode.sheet))}catch(c){console.warn(`Cannot get CSS styles from text's parentNode. Error: ${c}`,e)}l=Re(l,rt())}return u&&(l="SCRIPT_PLACEHOLDER"),!s&&!u&&l&&n&&(l=i?i(l,e.parentElement):l.replace(/[\S]/g,"*")),{type:A.Text,textContent:l||"",isStyle:s,rootId:o}}function zr(e,t){const{doc:r,blockClass:n,blockSelector:i,inlineStylesheet:o,maskInputOptions:a={},maskInputFn:l,dataURLOptions:s={},inlineImages:u,recordCanvas:c,keepIframeSrcFn:h,newlyAddedElement:m=!1,rootId:f}=t,g=Fr(e,n,i),p=Er(e);let y={};const w=e.attributes.length;for(let v=0;vI.href===e.href);let b=null;v&&(b=Te(v)),b&&(delete y.rel,delete y.href,y._cssText=Re(b,v.href))}if(p==="style"&&e.sheet&&!(e.innerText||e.textContent||"").trim().length){const v=Te(e.sheet);v&&(y._cssText=Re(v,rt()))}if(p==="input"||p==="textarea"||p==="select"){const v=e.value,b=e.checked;y.type!=="radio"&&y.type!=="checkbox"&&y.type!=="submit"&&y.type!=="button"&&v?y.value=et({element:e,type:tt(e),tagName:p,value:v,maskInputOptions:a,maskInputFn:l}):b&&(y.checked=b)}if(p==="option"&&(e.selected&&!a.select?y.selected=!0:delete y.selected),p==="canvas"&&c){if(e.__context==="2d")Mr(e)||(y.rr_dataURL=e.toDataURL(s.type,s.quality));else if(!("__context"in e)){const v=e.toDataURL(s.type,s.quality),b=document.createElement("canvas");b.width=e.width,b.height=e.height;const I=b.toDataURL(s.type,s.quality);v!==I&&(y.rr_dataURL=v)}}if(p==="img"&&u){fe||(fe=r.createElement("canvas"),Mt=fe.getContext("2d"));const v=e,b=v.crossOrigin;v.crossOrigin="anonymous";const I=()=>{v.removeEventListener("load",I);try{fe.width=v.naturalWidth,fe.height=v.naturalHeight,Mt.drawImage(v,0,0),y.rr_dataURL=fe.toDataURL(s.type,s.quality)}catch(B){console.warn(`Cannot inline img src=${v.currentSrc}! Error: ${B}`)}b?y.crossOrigin=b:v.removeAttribute("crossorigin")};v.complete&&v.naturalWidth!==0?I():v.addEventListener("load",I)}if(p==="audio"||p==="video"){const v=y;v.rr_mediaState=e.paused?"paused":"played",v.rr_mediaCurrentTime=e.currentTime,v.rr_mediaPlaybackRate=e.playbackRate,v.rr_mediaMuted=e.muted,v.rr_mediaLoop=e.loop,v.rr_mediaVolume=e.volume}if(m||(e.scrollLeft&&(y.rr_scrollLeft=e.scrollLeft),e.scrollTop&&(y.rr_scrollTop=e.scrollTop)),g){const{width:v,height:b}=e.getBoundingClientRect();y={class:y.class,rr_width:`${v}px`,rr_height:`${b}px`}}p==="iframe"&&!h(y.src)&&(e.contentDocument||(y.rr_src=y.src),delete y.src);let S;try{customElements.get(p)&&(S=!0)}catch{}return{type:A.Element,tagName:p,attributes:y,childNodes:[],isSVG:Lr(e)||void 0,needBlock:g,rootId:f,isCustom:S}}function x(e){return e==null?"":e.toLowerCase()}function qr(e,t){if(t.comment&&e.type===A.Comment)return!0;if(e.type===A.Element){if(t.script&&(e.tagName==="script"||e.tagName==="link"&&(e.attributes.rel==="preload"||e.attributes.rel==="modulepreload")&&e.attributes.as==="script"||e.tagName==="link"&&e.attributes.rel==="prefetch"&&typeof e.attributes.href=="string"&&wt(e.attributes.href)==="js"))return!0;if(t.headFavicon&&(e.tagName==="link"&&e.attributes.rel==="shortcut icon"||e.tagName==="meta"&&(x(e.attributes.name).match(/^msapplication-tile(image|color)$/)||x(e.attributes.name)==="application-name"||x(e.attributes.rel)==="icon"||x(e.attributes.rel)==="apple-touch-icon"||x(e.attributes.rel)==="shortcut icon")))return!0;if(e.tagName==="meta"){if(t.headMetaDescKeywords&&x(e.attributes.name).match(/^description|keywords$/))return!0;if(t.headMetaSocial&&(x(e.attributes.property).match(/^(og|twitter|fb):/)||x(e.attributes.name).match(/^(og|twitter):/)||x(e.attributes.name)==="pinterest"))return!0;if(t.headMetaRobots&&(x(e.attributes.name)==="robots"||x(e.attributes.name)==="googlebot"||x(e.attributes.name)==="bingbot"))return!0;if(t.headMetaHttpEquiv&&e.attributes["http-equiv"]!==void 0)return!0;if(t.headMetaAuthorship&&(x(e.attributes.name)==="author"||x(e.attributes.name)==="generator"||x(e.attributes.name)==="framework"||x(e.attributes.name)==="publisher"||x(e.attributes.name)==="progid"||x(e.attributes.property).match(/^article:/)||x(e.attributes.property).match(/^product:/)))return!0;if(t.headMetaVerification&&(x(e.attributes.name)==="google-site-verification"||x(e.attributes.name)==="yandex-verification"||x(e.attributes.name)==="csrf-token"||x(e.attributes.name)==="p:domain_verify"||x(e.attributes.name)==="verify-v1"||x(e.attributes.name)==="verification"||x(e.attributes.name)==="shopify-checkout-api-token"))return!0}}return!1}function pe(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,maskTextClass:a,maskTextSelector:l,skipChild:s=!1,inlineStylesheet:u=!0,maskInputOptions:c={},maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g={},inlineImages:p=!1,recordCanvas:y=!1,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v=5e3,onStylesheetLoad:b,stylesheetLoadTimeout:I=5e3,keepIframeSrcFn:B=()=>!1,newlyAddedElement:P=!1}=t;let{needsMask:k}=t,{preserveWhiteSpace:T=!0}=t;!k&&e.childNodes&&(k=Et(e,a,l,k===void 0));const G=Wr(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,dataURLOptions:g,inlineImages:p,recordCanvas:y,keepIframeSrcFn:B,newlyAddedElement:P});if(!G)return console.warn(e,"not serialized"),null;let V;n.hasNode(e)?V=n.getId(e):qr(G,f)||!T&&G.type===A.Text&&!G.isStyle&&!G.textContent.replace(/^\s+|\s+$/gm,"").length?V=Ie:V=It();const O=Object.assign(G,{id:V});if(n.add(e,O),V===Ie)return null;w&&w(e);let le=!s;if(O.type===A.Element){le=le&&!O.needBlock,delete O.needBlock;const z=e.shadowRoot;z&&we(z)&&(O.isShadowHost=!0)}if((O.type===A.Document||O.type===A.Element)&&le){f.headWhitespace&&O.type===A.Element&&O.tagName==="head"&&(T=!1);const z={doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:s,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B};if(!(O.type===A.Element&&O.tagName==="textarea"&&O.attributes.value!==void 0))for(const ne of Array.from(e.childNodes)){const Z=pe(ne,z);Z&&O.childNodes.push(Z)}if(gr(e)&&e.shadowRoot)for(const ne of Array.from(e.shadowRoot.childNodes)){const Z=pe(ne,z);Z&&(we(e.shadowRoot)&&(Z.isShadow=!0),O.childNodes.push(Z))}}return e.parentNode&&be(e.parentNode)&&we(e.parentNode)&&(O.isShadow=!0),O.type===A.Element&&O.tagName==="iframe"&&Pr(e,()=>{const z=e.contentDocument;if(z&&S){const ne=pe(z,{doc:z,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B});ne&&S(e,ne)}},v),O.type===A.Element&&O.tagName==="link"&&typeof O.attributes.rel=="string"&&(O.attributes.rel==="stylesheet"||O.attributes.rel==="preload"&&typeof O.attributes.href=="string"&&wt(O.attributes.href)==="css")&&Br(e,()=>{if(b){const z=pe(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B});z&&b(e,z)}},I),O}function $r(e,t){const{mirror:r=new St,blockClass:n="rr-block",blockSelector:i=null,maskTextClass:o="rr-mask",maskTextSelector:a=null,inlineStylesheet:l=!0,inlineImages:s=!1,recordCanvas:u=!1,maskAllInputs:c=!1,maskTextFn:h,maskInputFn:m,slimDOM:f=!1,dataURLOptions:g,preserveWhiteSpace:p,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:S,onStylesheetLoad:v,stylesheetLoadTimeout:b,keepIframeSrcFn:I=()=>!1}=t||{};return pe(e,{doc:e,mirror:r,blockClass:n,blockSelector:i,maskTextClass:o,maskTextSelector:a,skipChild:!1,inlineStylesheet:l,maskInputOptions:c===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:c===!1?{password:!0}:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:f==="all",headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0}:f===!1?{}:f,dataURLOptions:g,inlineImages:s,recordCanvas:u,preserveWhiteSpace:p,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:S,onStylesheetLoad:v,stylesheetLoadTimeout:b,keepIframeSrcFn:I,newlyAddedElement:!1})}function U(e,t,r=document){const n={capture:!0,passive:!0};return r.addEventListener(e,t,n),()=>r.removeEventListener(e,t,n)}const me=`Please stop import mirror directly. Instead of that,\r +now you can use replayer.getMirror() to access the mirror instance of a replayer,\r +or you can use record.mirror to access the mirror instance during recording.`;let Ot={map:{},getId(){return console.error(me),-1},getNode(){return console.error(me),null},removeNodeFromMap(){console.error(me)},has(){return console.error(me),!1},reset(){console.error(me)}};typeof window<"u"&&window.Proxy&&window.Reflect&&(Ot=new Proxy(Ot,{get(e,t,r){return t==="map"&&console.error(me),Reflect.get(e,t,r)}}));function Me(e,t,r={}){let n=null,i=0;return function(...o){const a=Date.now();!i&&r.leading===!1&&(i=a);const l=t-(a-i),s=this;l<=0||l>t?(n&&(clearTimeout(n),n=null),i=a,e.apply(s,o)):!n&&r.trailing!==!1&&(n=setTimeout(()=>{i=r.leading===!1?0:Date.now(),n=null,e.apply(s,o)},l))}}function Ne(e,t,r,n,i=window){const o=i.Object.getOwnPropertyDescriptor(e,t);return i.Object.defineProperty(e,t,n?r:{set(a){setTimeout(()=>{r.set.call(this,a)},0),o&&o.set&&o.set.call(this,a)}}),()=>Ne(e,t,o||{},!0)}function ge(e,t,r){try{if(!(t in e))return()=>{};const n=e[t],i=r(n);return typeof i=="function"&&(i.prototype=i.prototype||{},Object.defineProperties(i,{__rrweb_original__:{enumerable:!1,value:n}})),e[t]=i,()=>{e[t]=n}}catch{return()=>{}}}let Ae=Date.now;/[1-9][0-9]{12}/.test(Date.now().toString())||(Ae=()=>new Date().getTime());function xt(e){var t,r,n,i,o,a;const l=e.document;return{left:l.scrollingElement?l.scrollingElement.scrollLeft:e.pageXOffset!==void 0?e.pageXOffset:l?.documentElement.scrollLeft||((r=(t=l?.body)===null||t===void 0?void 0:t.parentElement)===null||r===void 0?void 0:r.scrollLeft)||((n=l?.body)===null||n===void 0?void 0:n.scrollLeft)||0,top:l.scrollingElement?l.scrollingElement.scrollTop:e.pageYOffset!==void 0?e.pageYOffset:l?.documentElement.scrollTop||((o=(i=l?.body)===null||i===void 0?void 0:i.parentElement)===null||o===void 0?void 0:o.scrollTop)||((a=l?.body)===null||a===void 0?void 0:a.scrollTop)||0}}function kt(){return window.innerHeight||document.documentElement&&document.documentElement.clientHeight||document.body&&document.body.clientHeight}function Tt(){return window.innerWidth||document.documentElement&&document.documentElement.clientWidth||document.body&&document.body.clientWidth}function Rt(e){return e?e.nodeType===e.ELEMENT_NODE?e:e.parentElement:null}function H(e,t,r,n){if(!e)return!1;const i=Rt(e);if(!i)return!1;try{if(typeof t=="string"){if(i.classList.contains(t)||n&&i.closest("."+t)!==null)return!0}else if(De(i,t,n))return!0}catch{}return!!(r&&(i.matches(r)||n&&i.closest(r)!==null))}function jr(e,t){return t.getId(e)!==-1}function nt(e,t){return t.getId(e)===Ie}function Dt(e,t){if(be(e))return!1;const r=t.getId(e);return t.has(r)?e.parentNode&&e.parentNode.nodeType===e.DOCUMENT_NODE?!1:e.parentNode?Dt(e.parentNode,t):!0:!0}function it(e){return!!e.changedTouches}function Gr(e=window){"NodeList"in e&&!e.NodeList.prototype.forEach&&(e.NodeList.prototype.forEach=Array.prototype.forEach),"DOMTokenList"in e&&!e.DOMTokenList.prototype.forEach&&(e.DOMTokenList.prototype.forEach=Array.prototype.forEach),Node.prototype.contains||(Node.prototype.contains=(...t)=>{let r=t[0];if(!(0 in t))throw new TypeError("1 argument is required");do if(this===r)return!0;while(r=r&&r.parentNode);return!1})}function Nt(e,t){return!!(e.nodeName==="IFRAME"&&t.getMeta(e))}function At(e,t){return!!(e.nodeName==="LINK"&&e.nodeType===e.ELEMENT_NODE&&e.getAttribute&&e.getAttribute("rel")==="stylesheet"&&t.getMeta(e))}function ot(e){return!!e?.shadowRoot}class Vr{constructor(){this.id=1,this.styleIDMap=new WeakMap,this.idStyleMap=new Map}getId(t){var r;return(r=this.styleIDMap.get(t))!==null&&r!==void 0?r:-1}has(t){return this.styleIDMap.has(t)}add(t,r){if(this.has(t))return this.getId(t);let n;return r===void 0?n=this.id++:n=r,this.styleIDMap.set(t,n),this.idStyleMap.set(n,t),n}getStyle(t){return this.idStyleMap.get(t)||null}reset(){this.styleIDMap=new WeakMap,this.idStyleMap=new Map,this.id=1}generateId(){return this.id++}}function Lt(e){var t,r;let n=null;return((r=(t=e.getRootNode)===null||t===void 0?void 0:t.call(e))===null||r===void 0?void 0:r.nodeType)===Node.DOCUMENT_FRAGMENT_NODE&&e.getRootNode().host&&(n=e.getRootNode().host),n}function Jr(e){let t=e,r;for(;r=Lt(t);)t=r;return t}function Xr(e){const t=e.ownerDocument;if(!t)return!1;const r=Jr(e);return t.contains(r)}function Ft(e){const t=e.ownerDocument;return t?t.contains(e)||Xr(e):!1}var E=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(E||{}),C=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(C||{}),$=(e=>(e[e.MouseUp=0]="MouseUp",e[e.MouseDown=1]="MouseDown",e[e.Click=2]="Click",e[e.ContextMenu=3]="ContextMenu",e[e.DblClick=4]="DblClick",e[e.Focus=5]="Focus",e[e.Blur=6]="Blur",e[e.TouchStart=7]="TouchStart",e[e.TouchMove_Departed=8]="TouchMove_Departed",e[e.TouchEnd=9]="TouchEnd",e[e.TouchCancel=10]="TouchCancel",e))($||{}),ee=(e=>(e[e.Mouse=0]="Mouse",e[e.Pen=1]="Pen",e[e.Touch=2]="Touch",e))(ee||{}),ye=(e=>(e[e["2D"]=0]="2D",e[e.WebGL=1]="WebGL",e[e.WebGL2=2]="WebGL2",e))(ye||{});function Pt(e){return"__ln"in e}class Kr{constructor(){this.length=0,this.head=null,this.tail=null}get(t){if(t>=this.length)throw new Error("Position outside of list range");let r=this.head;for(let n=0;n`${e}@${t}`;class Yr{constructor(){this.frozen=!1,this.locked=!1,this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.mapRemoves=[],this.movedMap={},this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.processMutations=t=>{t.forEach(this.processMutation),this.emit()},this.emit=()=>{if(this.frozen||this.locked)return;const t=[],r=new Set,n=new Kr,i=s=>{let u=s,c=Ie;for(;c===Ie;)u=u&&u.nextSibling,c=u&&this.mirror.getId(u);return c},o=s=>{if(!s.parentNode||!Ft(s)||s.parentNode.tagName==="TEXTAREA")return;const u=be(s.parentNode)?this.mirror.getId(Lt(s)):this.mirror.getId(s.parentNode),c=i(s);if(u===-1||c===-1)return n.addNode(s);const h=pe(s,{doc:this.doc,mirror:this.mirror,blockClass:this.blockClass,blockSelector:this.blockSelector,maskTextClass:this.maskTextClass,maskTextSelector:this.maskTextSelector,skipChild:!0,newlyAddedElement:!0,inlineStylesheet:this.inlineStylesheet,maskInputOptions:this.maskInputOptions,maskTextFn:this.maskTextFn,maskInputFn:this.maskInputFn,slimDOMOptions:this.slimDOMOptions,dataURLOptions:this.dataURLOptions,recordCanvas:this.recordCanvas,inlineImages:this.inlineImages,onSerialize:m=>{Nt(m,this.mirror)&&this.iframeManager.addIframe(m),At(m,this.mirror)&&this.stylesheetManager.trackLinkElement(m),ot(s)&&this.shadowDomManager.addShadowRoot(s.shadowRoot,this.doc)},onIframeLoad:(m,f)=>{this.iframeManager.attachIframe(m,f),this.shadowDomManager.observeAttachShadow(m)},onStylesheetLoad:(m,f)=>{this.stylesheetManager.attachLinkElement(m,f)}});h&&(t.push({parentId:u,nextId:c,node:h}),r.add(h.id))};for(;this.mapRemoves.length;)this.mirror.removeNodeFromMap(this.mapRemoves.shift());for(const s of this.movedSet)Wt(this.removes,s,this.mirror)&&!this.movedSet.has(s.parentNode)||o(s);for(const s of this.addedSet)!Ht(this.droppedSet,s)&&!Wt(this.removes,s,this.mirror)||Ht(this.movedSet,s)?o(s):this.droppedSet.add(s);let a=null;for(;n.length;){let s=null;if(a){const u=this.mirror.getId(a.value.parentNode),c=i(a.value);u!==-1&&c!==-1&&(s=a)}if(!s){let u=n.tail;for(;u;){const c=u;if(u=u.previous,c){const h=this.mirror.getId(c.value.parentNode);if(i(c.value)===-1)continue;if(h!==-1){s=c;break}else{const f=c.value;if(f.parentNode&&f.parentNode.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const g=f.parentNode.host;if(this.mirror.getId(g)!==-1){s=c;break}}}}}}if(!s){for(;n.head;)n.removeNode(n.head.value);break}a=s.previous,n.removeNode(s.value),o(s.value)}const l={texts:this.texts.map(s=>{const u=s.node;return u.parentNode&&u.parentNode.tagName==="TEXTAREA"&&this.genTextAreaValueMutation(u.parentNode),{id:this.mirror.getId(u),value:s.value}}).filter(s=>!r.has(s.id)).filter(s=>this.mirror.has(s.id)),attributes:this.attributes.map(s=>{const{attributes:u}=s;if(typeof u.style=="string"){const c=JSON.stringify(s.styleDiff),h=JSON.stringify(s._unchangedStyles);c.length!r.has(s.id)).filter(s=>this.mirror.has(s.id)),removes:this.removes,adds:t};!l.texts.length&&!l.attributes.length&&!l.removes.length&&!l.adds.length||(this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.movedMap={},this.mutationCb(l))},this.genTextAreaValueMutation=t=>{let r=this.attributeMap.get(t);r||(r={node:t,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(r),this.attributeMap.set(t,r)),r.attributes.value=Array.from(t.childNodes,n=>n.textContent||"").join("")},this.processMutation=t=>{if(!nt(t.target,this.mirror))switch(t.type){case"characterData":{const r=t.target.textContent;!H(t.target,this.blockClass,this.blockSelector,!1)&&r!==t.oldValue&&this.texts.push({value:Et(t.target,this.maskTextClass,this.maskTextSelector,!0)&&r?this.maskTextFn?this.maskTextFn(r,Rt(t.target)):r.replace(/[\S]/g,"*"):r,node:t.target});break}case"attributes":{const r=t.target;let n=t.attributeName,i=t.target.getAttribute(n);if(n==="value"){const a=tt(r);i=et({element:r,maskInputOptions:this.maskInputOptions,tagName:r.tagName,type:a,value:i,maskInputFn:this.maskInputFn})}if(H(t.target,this.blockClass,this.blockSelector,!1)||i===t.oldValue)return;let o=this.attributeMap.get(t.target);if(r.tagName==="IFRAME"&&n==="src"&&!this.keepIframeSrcFn(i))if(!r.contentDocument)n="rr_src";else return;if(o||(o={node:t.target,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(o),this.attributeMap.set(t.target,o)),n==="type"&&r.tagName==="INPUT"&&(t.oldValue||"").toLowerCase()==="password"&&r.setAttribute("data-rr-is-password","true"),!_t(r.tagName,n)&&(o.attributes[n]=Ct(this.doc,ie(r.tagName),ie(n),i),n==="style")){if(!this.unattachedDoc)try{this.unattachedDoc=document.implementation.createHTMLDocument()}catch{this.unattachedDoc=this.doc}const a=this.unattachedDoc.createElement("span");t.oldValue&&a.setAttribute("style",t.oldValue);for(const l of Array.from(r.style)){const s=r.style.getPropertyValue(l),u=r.style.getPropertyPriority(l);s!==a.style.getPropertyValue(l)||u!==a.style.getPropertyPriority(l)?u===""?o.styleDiff[l]=s:o.styleDiff[l]=[s,u]:o._unchangedStyles[l]=[s,u]}for(const l of Array.from(a.style))r.style.getPropertyValue(l)===""&&(o.styleDiff[l]=!1)}break}case"childList":{if(H(t.target,this.blockClass,this.blockSelector,!0))return;if(t.target.tagName==="TEXTAREA"){this.genTextAreaValueMutation(t.target);return}t.addedNodes.forEach(r=>this.genAdds(r,t.target)),t.removedNodes.forEach(r=>{const n=this.mirror.getId(r),i=be(t.target)?this.mirror.getId(t.target.host):this.mirror.getId(t.target);H(t.target,this.blockClass,this.blockSelector,!1)||nt(r,this.mirror)||!jr(r,this.mirror)||(this.addedSet.has(r)?(st(this.addedSet,r),this.droppedSet.add(r)):this.addedSet.has(t.target)&&n===-1||Dt(t.target,this.mirror)||(this.movedSet.has(r)&&this.movedMap[Bt(n,i)]?st(this.movedSet,r):this.removes.push({parentId:i,id:n,isShadow:be(t.target)&&we(t.target)?!0:void 0})),this.mapRemoves.push(r))});break}}},this.genAdds=(t,r)=>{if(!this.processedNodeManager.inOtherBuffer(t,this)&&!(this.addedSet.has(t)||this.movedSet.has(t))){if(this.mirror.hasNode(t)){if(nt(t,this.mirror))return;this.movedSet.add(t);let n=null;r&&this.mirror.hasNode(r)&&(n=this.mirror.getId(r)),n&&n!==-1&&(this.movedMap[Bt(this.mirror.getId(t),n)]=!0)}else this.addedSet.add(t),this.droppedSet.delete(t);H(t,this.blockClass,this.blockSelector,!1)||(t.childNodes.forEach(n=>this.genAdds(n)),ot(t)&&t.shadowRoot.childNodes.forEach(n=>{this.processedNodeManager.add(n,this),this.genAdds(n,t)}))}}}init(t){["mutationCb","blockClass","blockSelector","maskTextClass","maskTextSelector","inlineStylesheet","maskInputOptions","maskTextFn","maskInputFn","keepIframeSrcFn","recordCanvas","inlineImages","slimDOMOptions","dataURLOptions","doc","mirror","iframeManager","stylesheetManager","shadowDomManager","canvasManager","processedNodeManager"].forEach(r=>{this[r]=t[r]})}freeze(){this.frozen=!0,this.canvasManager.freeze()}unfreeze(){this.frozen=!1,this.canvasManager.unfreeze(),this.emit()}isFrozen(){return this.frozen}lock(){this.locked=!0,this.canvasManager.lock()}unlock(){this.locked=!1,this.canvasManager.unlock(),this.emit()}reset(){this.shadowDomManager.reset(),this.canvasManager.reset()}}function st(e,t){e.delete(t),t.childNodes.forEach(r=>st(e,r))}function Wt(e,t,r){return e.length===0?!1:Ut(e,t,r)}function Ut(e,t,r){const{parentNode:n}=t;if(!n)return!1;const i=r.getId(n);return e.some(o=>o.id===i)?!0:Ut(e,n,r)}function Ht(e,t){return e.size===0?!1:zt(e,t)}function zt(e,t){const{parentNode:r}=t;return r?e.has(r)?!0:zt(e,r):!1}let Ce;function Qr(e){Ce=e}function Zr(){Ce=void 0}const _=e=>Ce?(...r)=>{try{return e(...r)}catch(n){if(Ce&&Ce(n)===!0)return;throw n}}:e,oe=[];function _e(e){try{if("composedPath"in e){const t=e.composedPath();if(t.length)return t[0]}else if("path"in e&&e.path.length)return e.path[0]}catch{}return e&&e.target}function qt(e,t){var r,n;const i=new Yr;oe.push(i),i.init(e);let o=window.MutationObserver||window.__rrMutationObserver;const a=(n=(r=window?.Zone)===null||r===void 0?void 0:r.__symbol__)===null||n===void 0?void 0:n.call(r,"MutationObserver");a&&window[a]&&(o=window[a]);const l=new o(_(i.processMutations.bind(i)));return l.observe(t,{attributes:!0,attributeOldValue:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0}),l}function en({mousemoveCb:e,sampling:t,doc:r,mirror:n}){if(t.mousemove===!1)return()=>{};const i=typeof t.mousemove=="number"?t.mousemove:50,o=typeof t.mousemoveCallback=="number"?t.mousemoveCallback:500;let a=[],l;const s=Me(_(h=>{const m=Date.now()-l;e(a.map(f=>(f.timeOffset-=m,f)),h),a=[],l=null}),o),u=_(Me(_(h=>{const m=_e(h),{clientX:f,clientY:g}=it(h)?h.changedTouches[0]:h;l||(l=Ae()),a.push({x:f,y:g,id:n.getId(m),timeOffset:Ae()-l}),s(typeof DragEvent<"u"&&h instanceof DragEvent?C.Drag:h instanceof MouseEvent?C.MouseMove:C.TouchMove)}),i,{trailing:!1})),c=[U("mousemove",u,r),U("touchmove",u,r),U("drag",u,r)];return _(()=>{c.forEach(h=>h())})}function tn({mouseInteractionCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){if(o.mouseInteraction===!1)return()=>{};const a=o.mouseInteraction===!0||o.mouseInteraction===void 0?{}:o.mouseInteraction,l=[];let s=null;const u=c=>h=>{const m=_e(h);if(H(m,n,i,!0))return;let f=null,g=c;if("pointerType"in h){switch(h.pointerType){case"mouse":f=ee.Mouse;break;case"touch":f=ee.Touch;break;case"pen":f=ee.Pen;break}f===ee.Touch?$[c]===$.MouseDown?g="TouchStart":$[c]===$.MouseUp&&(g="TouchEnd"):ee.Pen}else it(h)&&(f=ee.Touch);f!==null?(s=f,(g.startsWith("Touch")&&f===ee.Touch||g.startsWith("Mouse")&&f===ee.Mouse)&&(f=null)):$[c]===$.Click&&(f=s,s=null);const p=it(h)?h.changedTouches[0]:h;if(!p)return;const y=r.getId(m),{clientX:w,clientY:S}=p;_(e)(Object.assign({type:$[g],id:y,x:w,y:S},f!==null&&{pointerType:f}))};return Object.keys($).filter(c=>Number.isNaN(Number(c))&&!c.endsWith("_Departed")&&a[c]!==!1).forEach(c=>{let h=ie(c);const m=u(c);if(window.PointerEvent)switch($[c]){case $.MouseDown:case $.MouseUp:h=h.replace("mouse","pointer");break;case $.TouchStart:case $.TouchEnd:return}l.push(U(h,m,t))}),_(()=>{l.forEach(c=>c())})}function $t({scrollCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){const a=_(Me(_(l=>{const s=_e(l);if(!s||H(s,n,i,!0))return;const u=r.getId(s);if(s===t&&t.defaultView){const c=xt(t.defaultView);e({id:u,x:c.left,y:c.top})}else e({id:u,x:s.scrollLeft,y:s.scrollTop})}),o.scroll||100));return U("scroll",a,t)}function rn({viewportResizeCb:e},{win:t}){let r=-1,n=-1;const i=_(Me(_(()=>{const o=kt(),a=Tt();(r!==o||n!==a)&&(e({width:Number(a),height:Number(o)}),r=o,n=a)}),200));return U("resize",i,t)}const nn=["INPUT","TEXTAREA","SELECT"],jt=new WeakMap;function on({inputCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,ignoreClass:o,ignoreSelector:a,maskInputOptions:l,maskInputFn:s,sampling:u,userTriggeredOnInput:c}){function h(S){let v=_e(S);const b=S.isTrusted,I=v&&v.tagName;if(v&&I==="OPTION"&&(v=v.parentElement),!v||!I||nn.indexOf(I)<0||H(v,n,i,!0)||v.classList.contains(o)||a&&v.matches(a))return;let B=v.value,P=!1;const k=tt(v)||"";k==="radio"||k==="checkbox"?P=v.checked:(l[I.toLowerCase()]||l[k])&&(B=et({element:v,maskInputOptions:l,tagName:I,type:k,value:B,maskInputFn:s})),m(v,c?{text:B,isChecked:P,userTriggered:b}:{text:B,isChecked:P});const T=v.name;k==="radio"&&T&&P&&t.querySelectorAll(`input[type="radio"][name="${T}"]`).forEach(G=>{if(G!==v){const V=G.value;m(G,c?{text:V,isChecked:!P,userTriggered:!1}:{text:V,isChecked:!P})}})}function m(S,v){const b=jt.get(S);if(!b||b.text!==v.text||b.isChecked!==v.isChecked){jt.set(S,v);const I=r.getId(S);_(e)(Object.assign(Object.assign({},v),{id:I}))}}const g=(u.input==="last"?["change"]:["input","change"]).map(S=>U(S,_(h),t)),p=t.defaultView;if(!p)return()=>{g.forEach(S=>S())};const y=p.Object.getOwnPropertyDescriptor(p.HTMLInputElement.prototype,"value"),w=[[p.HTMLInputElement.prototype,"value"],[p.HTMLInputElement.prototype,"checked"],[p.HTMLSelectElement.prototype,"value"],[p.HTMLTextAreaElement.prototype,"value"],[p.HTMLSelectElement.prototype,"selectedIndex"],[p.HTMLOptionElement.prototype,"selected"]];return y&&y.set&&g.push(...w.map(S=>Ne(S[0],S[1],{set(){_(h)({target:this,isTrusted:!1})}},!1,p))),_(()=>{g.forEach(S=>S())})}function Le(e){const t=[];function r(n,i){if(Fe("CSSGroupingRule")&&n.parentRule instanceof CSSGroupingRule||Fe("CSSMediaRule")&&n.parentRule instanceof CSSMediaRule||Fe("CSSSupportsRule")&&n.parentRule instanceof CSSSupportsRule||Fe("CSSConditionRule")&&n.parentRule instanceof CSSConditionRule){const a=Array.from(n.parentRule.cssRules).indexOf(n);i.unshift(a)}else if(n.parentStyleSheet){const a=Array.from(n.parentStyleSheet.cssRules).indexOf(n);i.unshift(a)}return i}return r(e,t)}function te(e,t,r){let n,i;return e?(e.ownerNode?n=t.getId(e.ownerNode):i=r.getId(e),{styleId:i,id:n}):{}}function sn({styleSheetRuleCb:e,mirror:t,stylesheetManager:r},{win:n}){if(!n.CSSStyleSheet||!n.CSSStyleSheet.prototype)return()=>{};const i=n.CSSStyleSheet.prototype.insertRule;n.CSSStyleSheet.prototype.insertRule=new Proxy(i,{apply:_((c,h,m)=>{const[f,g]=m,{id:p,styleId:y}=te(h,t,r.styleMirror);return(p&&p!==-1||y&&y!==-1)&&e({id:p,styleId:y,adds:[{rule:f,index:g}]}),c.apply(h,m)})});const o=n.CSSStyleSheet.prototype.deleteRule;n.CSSStyleSheet.prototype.deleteRule=new Proxy(o,{apply:_((c,h,m)=>{const[f]=m,{id:g,styleId:p}=te(h,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,removes:[{index:f}]}),c.apply(h,m)})});let a;n.CSSStyleSheet.prototype.replace&&(a=n.CSSStyleSheet.prototype.replace,n.CSSStyleSheet.prototype.replace=new Proxy(a,{apply:_((c,h,m)=>{const[f]=m,{id:g,styleId:p}=te(h,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,replace:f}),c.apply(h,m)})}));let l;n.CSSStyleSheet.prototype.replaceSync&&(l=n.CSSStyleSheet.prototype.replaceSync,n.CSSStyleSheet.prototype.replaceSync=new Proxy(l,{apply:_((c,h,m)=>{const[f]=m,{id:g,styleId:p}=te(h,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,replaceSync:f}),c.apply(h,m)})}));const s={};Pe("CSSGroupingRule")?s.CSSGroupingRule=n.CSSGroupingRule:(Pe("CSSMediaRule")&&(s.CSSMediaRule=n.CSSMediaRule),Pe("CSSConditionRule")&&(s.CSSConditionRule=n.CSSConditionRule),Pe("CSSSupportsRule")&&(s.CSSSupportsRule=n.CSSSupportsRule));const u={};return Object.entries(s).forEach(([c,h])=>{u[c]={insertRule:h.prototype.insertRule,deleteRule:h.prototype.deleteRule},h.prototype.insertRule=new Proxy(u[c].insertRule,{apply:_((m,f,g)=>{const[p,y]=g,{id:w,styleId:S}=te(f.parentStyleSheet,t,r.styleMirror);return(w&&w!==-1||S&&S!==-1)&&e({id:w,styleId:S,adds:[{rule:p,index:[...Le(f),y||0]}]}),m.apply(f,g)})}),h.prototype.deleteRule=new Proxy(u[c].deleteRule,{apply:_((m,f,g)=>{const[p]=g,{id:y,styleId:w}=te(f.parentStyleSheet,t,r.styleMirror);return(y&&y!==-1||w&&w!==-1)&&e({id:y,styleId:w,removes:[{index:[...Le(f),p]}]}),m.apply(f,g)})})}),_(()=>{n.CSSStyleSheet.prototype.insertRule=i,n.CSSStyleSheet.prototype.deleteRule=o,a&&(n.CSSStyleSheet.prototype.replace=a),l&&(n.CSSStyleSheet.prototype.replaceSync=l),Object.entries(s).forEach(([c,h])=>{h.prototype.insertRule=u[c].insertRule,h.prototype.deleteRule=u[c].deleteRule})})}function Gt({mirror:e,stylesheetManager:t},r){var n,i,o;let a=null;r.nodeName==="#document"?a=e.getId(r):a=e.getId(r.host);const l=r.nodeName==="#document"?(n=r.defaultView)===null||n===void 0?void 0:n.Document:(o=(i=r.ownerDocument)===null||i===void 0?void 0:i.defaultView)===null||o===void 0?void 0:o.ShadowRoot,s=l?.prototype?Object.getOwnPropertyDescriptor(l?.prototype,"adoptedStyleSheets"):void 0;return a===null||a===-1||!l||!s?()=>{}:(Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get(){var u;return(u=s.get)===null||u===void 0?void 0:u.call(this)},set(u){var c;const h=(c=s.set)===null||c===void 0?void 0:c.call(this,u);if(a!==null&&a!==-1)try{t.adoptStyleSheets(u,a)}catch{}return h}}),_(()=>{Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get:s.get,set:s.set})}))}function an({styleDeclarationCb:e,mirror:t,ignoreCSSAttributes:r,stylesheetManager:n},{win:i}){const o=i.CSSStyleDeclaration.prototype.setProperty;i.CSSStyleDeclaration.prototype.setProperty=new Proxy(o,{apply:_((l,s,u)=>{var c;const[h,m,f]=u;if(r.has(h))return o.apply(s,[h,m,f]);const{id:g,styleId:p}=te((c=s.parentRule)===null||c===void 0?void 0:c.parentStyleSheet,t,n.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,set:{property:h,value:m,priority:f},index:Le(s.parentRule)}),l.apply(s,u)})});const a=i.CSSStyleDeclaration.prototype.removeProperty;return i.CSSStyleDeclaration.prototype.removeProperty=new Proxy(a,{apply:_((l,s,u)=>{var c;const[h]=u;if(r.has(h))return a.apply(s,[h]);const{id:m,styleId:f}=te((c=s.parentRule)===null||c===void 0?void 0:c.parentStyleSheet,t,n.styleMirror);return(m&&m!==-1||f&&f!==-1)&&e({id:m,styleId:f,remove:{property:h},index:Le(s.parentRule)}),l.apply(s,u)})}),_(()=>{i.CSSStyleDeclaration.prototype.setProperty=o,i.CSSStyleDeclaration.prototype.removeProperty=a})}function ln({mediaInteractionCb:e,blockClass:t,blockSelector:r,mirror:n,sampling:i,doc:o}){const a=_(s=>Me(_(u=>{const c=_e(u);if(!c||H(c,t,r,!0))return;const{currentTime:h,volume:m,muted:f,playbackRate:g,loop:p}=c;e({type:s,id:n.getId(c),currentTime:h,volume:m,muted:f,playbackRate:g,loop:p})}),i.media||500)),l=[U("play",a(0),o),U("pause",a(1),o),U("seeked",a(2),o),U("volumechange",a(3),o),U("ratechange",a(4),o)];return _(()=>{l.forEach(s=>s())})}function un({fontCb:e,doc:t}){const r=t.defaultView;if(!r)return()=>{};const n=[],i=new WeakMap,o=r.FontFace;r.FontFace=function(s,u,c){const h=new o(s,u,c);return i.set(h,{family:s,buffer:typeof u!="string",descriptors:c,fontSource:typeof u=="string"?u:JSON.stringify(Array.from(new Uint8Array(u)))}),h};const a=ge(t.fonts,"add",function(l){return function(s){return setTimeout(_(()=>{const u=i.get(s);u&&(e(u),i.delete(s))}),0),l.apply(this,[s])}});return n.push(()=>{r.FontFace=o}),n.push(a),_(()=>{n.forEach(l=>l())})}function cn(e){const{doc:t,mirror:r,blockClass:n,blockSelector:i,selectionCb:o}=e;let a=!0;const l=_(()=>{const s=t.getSelection();if(!s||a&&s?.isCollapsed)return;a=s.isCollapsed||!1;const u=[],c=s.rangeCount||0;for(let h=0;h{}:ge(r.customElements,"define",function(i){return function(o,a,l){try{t({define:{name:o}})}catch{console.warn(`Custom element callback failed for ${o}`)}return i.apply(this,[o,a,l])}})}function fn(e,t){const{mutationCb:r,mousemoveCb:n,mouseInteractionCb:i,scrollCb:o,viewportResizeCb:a,inputCb:l,mediaInteractionCb:s,styleSheetRuleCb:u,styleDeclarationCb:c,canvasMutationCb:h,fontCb:m,selectionCb:f,customElementCb:g}=e;e.mutationCb=(...p)=>{t.mutation&&t.mutation(...p),r(...p)},e.mousemoveCb=(...p)=>{t.mousemove&&t.mousemove(...p),n(...p)},e.mouseInteractionCb=(...p)=>{t.mouseInteraction&&t.mouseInteraction(...p),i(...p)},e.scrollCb=(...p)=>{t.scroll&&t.scroll(...p),o(...p)},e.viewportResizeCb=(...p)=>{t.viewportResize&&t.viewportResize(...p),a(...p)},e.inputCb=(...p)=>{t.input&&t.input(...p),l(...p)},e.mediaInteractionCb=(...p)=>{t.mediaInteaction&&t.mediaInteaction(...p),s(...p)},e.styleSheetRuleCb=(...p)=>{t.styleSheetRule&&t.styleSheetRule(...p),u(...p)},e.styleDeclarationCb=(...p)=>{t.styleDeclaration&&t.styleDeclaration(...p),c(...p)},e.canvasMutationCb=(...p)=>{t.canvasMutation&&t.canvasMutation(...p),h(...p)},e.fontCb=(...p)=>{t.font&&t.font(...p),m(...p)},e.selectionCb=(...p)=>{t.selection&&t.selection(...p),f(...p)},e.customElementCb=(...p)=>{t.customElement&&t.customElement(...p),g(...p)}}function hn(e,t={}){const r=e.doc.defaultView;if(!r)return()=>{};fn(e,t);let n;e.recordDOM&&(n=qt(e,e.doc));const i=en(e),o=tn(e),a=$t(e),l=rn(e,{win:r}),s=on(e),u=ln(e);let c=()=>{},h=()=>{},m=()=>{},f=()=>{};e.recordDOM&&(c=sn(e,{win:r}),h=Gt(e,e.doc),m=an(e,{win:r}),e.collectFonts&&(f=un(e)));const g=cn(e),p=dn(e),y=[];for(const w of e.plugins)y.push(w.observer(w.callback,r,w.options));return _(()=>{oe.forEach(w=>w.reset()),n?.disconnect(),i(),o(),a(),l(),s(),u(),c(),h(),m(),f(),g(),p(),y.forEach(w=>w())})}function Fe(e){return typeof window[e]<"u"}function Pe(e){return!!(typeof window[e]<"u"&&window[e].prototype&&"insertRule"in window[e].prototype&&"deleteRule"in window[e].prototype)}class Vt{constructor(t){this.generateIdFn=t,this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap}getId(t,r,n,i){const o=n||this.getIdToRemoteIdMap(t),a=i||this.getRemoteIdToIdMap(t);let l=o.get(r);return l||(l=this.generateIdFn(),o.set(r,l),a.set(l,r)),l}getIds(t,r){const n=this.getIdToRemoteIdMap(t),i=this.getRemoteIdToIdMap(t);return r.map(o=>this.getId(t,o,n,i))}getRemoteId(t,r,n){const i=n||this.getRemoteIdToIdMap(t);if(typeof r!="number")return r;const o=i.get(r);return o||-1}getRemoteIds(t,r){const n=this.getRemoteIdToIdMap(t);return r.map(i=>this.getRemoteId(t,i,n))}reset(t){if(!t){this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap;return}this.iframeIdToRemoteIdMap.delete(t),this.iframeRemoteIdToIdMap.delete(t)}getIdToRemoteIdMap(t){let r=this.iframeIdToRemoteIdMap.get(t);return r||(r=new Map,this.iframeIdToRemoteIdMap.set(t,r)),r}getRemoteIdToIdMap(t){let r=this.iframeRemoteIdToIdMap.get(t);return r||(r=new Map,this.iframeRemoteIdToIdMap.set(t,r)),r}}class pn{constructor(t){this.iframes=new WeakMap,this.crossOriginIframeMap=new WeakMap,this.crossOriginIframeMirror=new Vt(It),this.crossOriginIframeRootIdMap=new WeakMap,this.mutationCb=t.mutationCb,this.wrappedEmit=t.wrappedEmit,this.stylesheetManager=t.stylesheetManager,this.recordCrossOriginIframes=t.recordCrossOriginIframes,this.crossOriginIframeStyleMirror=new Vt(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)),this.mirror=t.mirror,this.recordCrossOriginIframes&&window.addEventListener("message",this.handleMessage.bind(this))}addIframe(t){this.iframes.set(t,!0),t.contentWindow&&this.crossOriginIframeMap.set(t.contentWindow,t)}addLoadListener(t){this.loadListener=t}attachIframe(t,r){var n;this.mutationCb({adds:[{parentId:this.mirror.getId(t),nextId:null,node:r}],removes:[],texts:[],attributes:[],isAttachIframe:!0}),(n=this.loadListener)===null||n===void 0||n.call(this,t),t.contentDocument&&t.contentDocument.adoptedStyleSheets&&t.contentDocument.adoptedStyleSheets.length>0&&this.stylesheetManager.adoptStyleSheets(t.contentDocument.adoptedStyleSheets,this.mirror.getId(t.contentDocument))}handleMessage(t){const r=t;if(r.data.type!=="rrweb"||r.origin!==r.data.origin||!t.source)return;const i=this.crossOriginIframeMap.get(t.source);if(!i)return;const o=this.transformCrossOriginEvent(i,r.data.event);o&&this.wrappedEmit(o,r.data.isCheckout)}transformCrossOriginEvent(t,r){var n;switch(r.type){case E.FullSnapshot:{this.crossOriginIframeMirror.reset(t),this.crossOriginIframeStyleMirror.reset(t),this.replaceIdOnNode(r.data.node,t);const i=r.data.node.id;return this.crossOriginIframeRootIdMap.set(t,i),this.patchRootIdOnNode(r.data.node,i),{timestamp:r.timestamp,type:E.IncrementalSnapshot,data:{source:C.Mutation,adds:[{parentId:this.mirror.getId(t),nextId:null,node:r.data.node}],removes:[],texts:[],attributes:[],isAttachIframe:!0}}}case E.Meta:case E.Load:case E.DomContentLoaded:return!1;case E.Plugin:return r;case E.Custom:return this.replaceIds(r.data.payload,t,["id","parentId","previousId","nextId"]),r;case E.IncrementalSnapshot:switch(r.data.source){case C.Mutation:return r.data.adds.forEach(i=>{this.replaceIds(i,t,["parentId","nextId","previousId"]),this.replaceIdOnNode(i.node,t);const o=this.crossOriginIframeRootIdMap.get(t);o&&this.patchRootIdOnNode(i.node,o)}),r.data.removes.forEach(i=>{this.replaceIds(i,t,["parentId","id"])}),r.data.attributes.forEach(i=>{this.replaceIds(i,t,["id"])}),r.data.texts.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.Drag:case C.TouchMove:case C.MouseMove:return r.data.positions.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.ViewportResize:return!1;case C.MediaInteraction:case C.MouseInteraction:case C.Scroll:case C.CanvasMutation:case C.Input:return this.replaceIds(r.data,t,["id"]),r;case C.StyleSheetRule:case C.StyleDeclaration:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleId"]),r;case C.Font:return r;case C.Selection:return r.data.ranges.forEach(i=>{this.replaceIds(i,t,["start","end"])}),r;case C.AdoptedStyleSheet:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleIds"]),(n=r.data.styles)===null||n===void 0||n.forEach(i=>{this.replaceStyleIds(i,t,["styleId"])}),r}}return!1}replace(t,r,n,i){for(const o of i)!Array.isArray(r[o])&&typeof r[o]!="number"||(Array.isArray(r[o])?r[o]=t.getIds(n,r[o]):r[o]=t.getId(n,r[o]));return r}replaceIds(t,r,n){return this.replace(this.crossOriginIframeMirror,t,r,n)}replaceStyleIds(t,r,n){return this.replace(this.crossOriginIframeStyleMirror,t,r,n)}replaceIdOnNode(t,r){this.replaceIds(t,r,["id","rootId"]),"childNodes"in t&&t.childNodes.forEach(n=>{this.replaceIdOnNode(n,r)})}patchRootIdOnNode(t,r){t.type!==A.Document&&!t.rootId&&(t.rootId=r),"childNodes"in t&&t.childNodes.forEach(n=>{this.patchRootIdOnNode(n,r)})}}class mn{constructor(t){this.shadowDoms=new WeakSet,this.restoreHandlers=[],this.mutationCb=t.mutationCb,this.scrollCb=t.scrollCb,this.bypassOptions=t.bypassOptions,this.mirror=t.mirror,this.init()}init(){this.reset(),this.patchAttachShadow(Element,document)}addShadowRoot(t,r){if(!we(t)||this.shadowDoms.has(t))return;this.shadowDoms.add(t);const n=qt(Object.assign(Object.assign({},this.bypassOptions),{doc:r,mutationCb:this.mutationCb,mirror:this.mirror,shadowDomManager:this}),t);this.restoreHandlers.push(()=>n.disconnect()),this.restoreHandlers.push($t(Object.assign(Object.assign({},this.bypassOptions),{scrollCb:this.scrollCb,doc:t,mirror:this.mirror}))),setTimeout(()=>{t.adoptedStyleSheets&&t.adoptedStyleSheets.length>0&&this.bypassOptions.stylesheetManager.adoptStyleSheets(t.adoptedStyleSheets,this.mirror.getId(t.host)),this.restoreHandlers.push(Gt({mirror:this.mirror,stylesheetManager:this.bypassOptions.stylesheetManager},t))},0)}observeAttachShadow(t){!t.contentWindow||!t.contentDocument||this.patchAttachShadow(t.contentWindow.Element,t.contentDocument)}patchAttachShadow(t,r){const n=this;this.restoreHandlers.push(ge(t.prototype,"attachShadow",function(i){return function(o){const a=i.call(this,o);return this.shadowRoot&&Ft(this)&&n.addShadowRoot(this.shadowRoot,r),a}}))}reset(){this.restoreHandlers.forEach(t=>{try{t()}catch{}}),this.restoreHandlers=[],this.shadowDoms=new WeakSet}}/*! ***************************************************************************** +>>>>>>> 571d8b5 (cleanup, draft) Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any @@ -13,10 +19,17 @@ or you can use record.mirror to access the mirror instance during recording.`;le LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +<<<<<<< HEAD ***************************************************************************** */function an(e,t){var r={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var i=0,n=Object.getOwnPropertySymbols(e);i"u"?[]:new Uint8Array(256),Ae=0;Ae>2],i+=me[(t[r]&3)<<4|t[r+1]>>4],i+=me[(t[r+1]&15)<<2|t[r+2]>>6],i+=me[t[r+2]&63];return n%3===2?i=i.substring(0,i.length-1)+"=":n%3===1&&(i=i.substring(0,i.length-2)+"=="),i};const zt=new Map;function dn(e,t){let r=zt.get(e);return r||(r=new Map,zt.set(e,r)),r.has(t)||r.set(t,[]),r.get(t)}const $t=(e,t,r)=>{if(!e||!(jt(e,t)||typeof e=="object"))return;const n=e.constructor.name,i=dn(r,n);let o=i.indexOf(e);return o===-1&&(o=i.length,i.push(e)),o};function Le(e,t,r){if(e instanceof Array)return e.map(n=>Le(n,t,r));if(e===null)return e;if(e instanceof Float32Array||e instanceof Float64Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Uint8Array||e instanceof Uint16Array||e instanceof Int16Array||e instanceof Int8Array||e instanceof Uint8ClampedArray)return{rr_type:e.constructor.name,args:[Object.values(e)]};if(e instanceof ArrayBuffer){const n=e.constructor.name,i=un(e);return{rr_type:n,base64:i}}else{if(e instanceof DataView)return{rr_type:e.constructor.name,args:[Le(e.buffer,t,r),e.byteOffset,e.byteLength]};if(e instanceof HTMLImageElement){const n=e.constructor.name,{src:i}=e;return{rr_type:n,src:i}}else if(e instanceof HTMLCanvasElement){const n="HTMLImageElement",i=e.toDataURL();return{rr_type:n,src:i}}else{if(e instanceof ImageData)return{rr_type:e.constructor.name,args:[Le(e.data,t,r),e.width,e.height]};if(jt(e,t)||typeof e=="object"){const n=e.constructor.name,i=$t(e,t,r);return{rr_type:n,index:i}}}}return e}const Gt=(e,t,r)=>e.map(n=>Le(n,t,r)),jt=(e,t)=>!!["WebGLActiveInfo","WebGLBuffer","WebGLFramebuffer","WebGLProgram","WebGLRenderbuffer","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebGLVertexArrayObject","WebGLVertexArrayObjectOES"].filter(i=>typeof t[i]=="function").find(i=>e instanceof t[i]);function fn(e,t,r,n){const i=[],o=Object.getOwnPropertyNames(t.CanvasRenderingContext2D.prototype);for(const a of o)try{if(typeof t.CanvasRenderingContext2D.prototype[a]!="function")continue;const l=he(t.CanvasRenderingContext2D.prototype,a,function(s){return function(...c){return U(this.canvas,r,n,!0)||setTimeout(()=>{const u=Gt(c,t,this);e(this.canvas,{type:pe["2D"],property:a,args:u})},0),s.apply(this,c)}});i.push(l)}catch{const s=ke(t.CanvasRenderingContext2D.prototype,a,{set(c){e(this.canvas,{type:pe["2D"],property:a,args:[c],setter:!0})}});i.push(s)}return()=>{i.forEach(a=>a())}}function hn(e){return e==="experimental-webgl"?"webgl":e}function Vt(e,t,r,n){const i=[];try{const o=he(e.HTMLCanvasElement.prototype,"getContext",function(a){return function(l,...s){if(!U(this,t,r,!0)){const c=hn(l);if("__context"in this||(this.__context=c),n&&["webgl","webgl2"].includes(c))if(s[0]&&typeof s[0]=="object"){const u=s[0];u.preserveDrawingBuffer||(u.preserveDrawingBuffer=!0)}else s.splice(0,1,{preserveDrawingBuffer:!0})}return a.apply(this,[l,...s])}});i.push(o)}catch{console.error("failed to patch HTMLCanvasElement.prototype.getContext")}return()=>{i.forEach(o=>o())}}function qt(e,t,r,n,i,o,a){const l=[],s=Object.getOwnPropertyNames(e);for(const c of s)if(!["isContextLost","canvas","drawingBufferWidth","drawingBufferHeight"].includes(c))try{if(typeof e[c]!="function")continue;const u=he(e,c,function(p){return function(...m){const f=p.apply(this,m);if($t(f,a,this),"tagName"in this.canvas&&!U(this.canvas,n,i,!0)){const g=Gt(m,a,this),h={type:t,property:c,args:g};r(this.canvas,h)}return f}});l.push(u)}catch{const p=ke(e,c,{set(m){r(this.canvas,{type:t,property:c,args:[m],setter:!0})}});l.push(p)}return l}function pn(e,t,r,n,i){const o=[];return o.push(...qt(t.WebGLRenderingContext.prototype,pe.WebGL,e,r,n,i,t)),typeof t.WebGL2RenderingContext<"u"&&o.push(...qt(t.WebGL2RenderingContext.prototype,pe.WebGL2,e,r,n,i,t)),()=>{o.forEach(a=>a())}}function mn(e,t){var r=t===void 0?null:t,n=e.toString(),i=n.split(` `);i.pop(),i.shift();for(var o=i[0].search(/\S/),a=/(['"])__worker_loader_strict__(['"])/g,l=0,s=i.length;l"u"?[]:new Uint8Array(256),Be=0;Be>2],i+=ve[(t[r]&3)<<4|t[r+1]>>4],i+=ve[(t[r+1]&15)<<2|t[r+2]>>6],i+=ve[t[r+2]&63];return n%3===2?i=i.substring(0,i.length-1)+"=":n%3===1&&(i=i.substring(0,i.length-2)+"=="),i};const Jt=new Map;function bn(e,t){let r=Jt.get(e);return r||(r=new Map,Jt.set(e,r)),r.has(t)||r.set(t,[]),r.get(t)}const Xt=(e,t,r)=>{if(!e||!(Yt(e,t)||typeof e=="object"))return;const n=e.constructor.name,i=bn(r,n);let o=i.indexOf(e);return o===-1&&(o=i.length,i.push(e)),o};function We(e,t,r){if(e instanceof Array)return e.map(n=>We(n,t,r));if(e===null)return e;if(e instanceof Float32Array||e instanceof Float64Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Uint8Array||e instanceof Uint16Array||e instanceof Int16Array||e instanceof Int8Array||e instanceof Uint8ClampedArray)return{rr_type:e.constructor.name,args:[Object.values(e)]};if(e instanceof ArrayBuffer){const n=e.constructor.name,i=Sn(e);return{rr_type:n,base64:i}}else{if(e instanceof DataView)return{rr_type:e.constructor.name,args:[We(e.buffer,t,r),e.byteOffset,e.byteLength]};if(e instanceof HTMLImageElement){const n=e.constructor.name,{src:i}=e;return{rr_type:n,src:i}}else if(e instanceof HTMLCanvasElement){const n="HTMLImageElement",i=e.toDataURL();return{rr_type:n,src:i}}else{if(e instanceof ImageData)return{rr_type:e.constructor.name,args:[We(e.data,t,r),e.width,e.height]};if(Yt(e,t)||typeof e=="object"){const n=e.constructor.name,i=Xt(e,t,r);return{rr_type:n,index:i}}}}return e}const Kt=(e,t,r)=>e.map(n=>We(n,t,r)),Yt=(e,t)=>!!["WebGLActiveInfo","WebGLBuffer","WebGLFramebuffer","WebGLProgram","WebGLRenderbuffer","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebGLVertexArrayObject","WebGLVertexArrayObjectOES"].filter(i=>typeof t[i]=="function").find(i=>e instanceof t[i]);function wn(e,t,r,n){const i=[],o=Object.getOwnPropertyNames(t.CanvasRenderingContext2D.prototype);for(const a of o)try{if(typeof t.CanvasRenderingContext2D.prototype[a]!="function")continue;const l=ge(t.CanvasRenderingContext2D.prototype,a,function(s){return function(...u){return H(this.canvas,r,n,!0)||setTimeout(()=>{const c=Kt(u,t,this);e(this.canvas,{type:ye["2D"],property:a,args:c})},0),s.apply(this,u)}});i.push(l)}catch{const s=Ne(t.CanvasRenderingContext2D.prototype,a,{set(u){e(this.canvas,{type:ye["2D"],property:a,args:[u],setter:!0})}});i.push(s)}return()=>{i.forEach(a=>a())}}function In(e){return e==="experimental-webgl"?"webgl":e}function Qt(e,t,r,n){const i=[];try{const o=ge(e.HTMLCanvasElement.prototype,"getContext",function(a){return function(l,...s){if(!H(this,t,r,!0)){const u=In(l);if("__context"in this||(this.__context=u),n&&["webgl","webgl2"].includes(u))if(s[0]&&typeof s[0]=="object"){const c=s[0];c.preserveDrawingBuffer||(c.preserveDrawingBuffer=!0)}else s.splice(0,1,{preserveDrawingBuffer:!0})}return a.apply(this,[l,...s])}});i.push(o)}catch{console.error("failed to patch HTMLCanvasElement.prototype.getContext")}return()=>{i.forEach(o=>o())}}function Zt(e,t,r,n,i,o,a){const l=[],s=Object.getOwnPropertyNames(e);for(const u of s)if(!["isContextLost","canvas","drawingBufferWidth","drawingBufferHeight"].includes(u))try{if(typeof e[u]!="function")continue;const c=ge(e,u,function(h){return function(...m){const f=h.apply(this,m);if(Xt(f,a,this),"tagName"in this.canvas&&!H(this.canvas,n,i,!0)){const g=Kt(m,a,this),p={type:t,property:u,args:g};r(this.canvas,p)}return f}});l.push(c)}catch{const h=Ne(e,u,{set(m){r(this.canvas,{type:t,property:u,args:[m],setter:!0})}});l.push(h)}return l}function Mn(e,t,r,n,i){const o=[];return o.push(...Zt(t.WebGLRenderingContext.prototype,ye.WebGL,e,r,n,i,t)),typeof t.WebGL2RenderingContext<"u"&&o.push(...Zt(t.WebGL2RenderingContext.prototype,ye.WebGL2,e,r,n,i,t)),()=>{o.forEach(a=>a())}}function Cn(e,t){var r=t===void 0?null:t,n=e.toString(),i=n.split(` +`);i.pop(),i.shift();for(var o=i[0].search(/\S/),a=/(['"])__worker_loader_strict__(['"])/g,l=0,s=i.length;l>>>>>> 571d8b5 (cleanup, draft) Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any @@ -29,6 +42,7 @@ or you can use record.mirror to access the mirror instance during recording.`;le LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +<<<<<<< HEAD ***************************************************************************** */function e(c,u,p,m){function f(g){return g instanceof p?g:new p(function(h){h(g)})}return new(p||(p=Promise))(function(g,h){function y(S){try{v(m.next(S))}catch(b){h(b)}}function w(S){try{v(m.throw(S))}catch(b){h(b)}}function v(S){S.done?g(S.value):f(S.value).then(y,w)}v((m=m.apply(c,u||[])).next())})}for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=typeof Uint8Array>"u"?[]:new Uint8Array(256),n=0;n>2],f+=t[(u[p]&3)<<4|u[p+1]>>4],f+=t[(u[p+1]&15)<<2|u[p+2]>>6],f+=t[u[p+2]&63];return m%3===2?f=f.substring(0,f.length-1)+"=":m%3===1&&(f=f.substring(0,f.length-2)+"=="),f};const o=new Map,a=new Map;function l(c,u,p){return e(this,void 0,void 0,function*(){const m=`${c}-${u}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const f=new OffscreenCanvas(c,u);f.getContext("2d");const h=yield(yield f.convertToBlob(p)).arrayBuffer(),y=i(h);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(c){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:u,bitmap:p,width:m,height:f,dataURLOptions:g}=c.data,h=l(m,f,g),y=new OffscreenCanvas(m,f);y.getContext("2d").drawImage(p,0,0),p.close();const v=yield y.convertToBlob(g),S=v.type,b=yield v.arrayBuffer(),M=i(b);if(!o.has(u)&&(yield h)===M)return o.set(u,M),s.postMessage({id:u});if(o.get(u)===M)return s.postMessage({id:u});s.postMessage({id:u,type:S,base64:M,width:m,height:f}),o.set(u,M)}else return s.postMessage({id:c.data.id})})}})()},null);class vn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,c)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(c)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Vt(r,n,i,!0),l=new Map,s=new Sn;s.onmessage=g=>{const{id:h}=g.data;if(l.set(h,!1),!("base64"in g.data))return;const{base64:y,type:w,width:v,height:S}=g.data;this.mutationCb({id:h,type:pe["2D"],commands:[{property:"clearRect",args:[0,0,v,S]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const c=1e3/t;let u=0,p;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(h=>{U(h,n,i,!0)||g.push(h)}),g},f=g=>{if(u&&g-uln(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(h);if(l.get(w)||h.width===0||h.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(h.__context)){const S=h.getContext(h.__context);((y=S?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&S.clear(S.COLOR_BUFFER_BIT)}const v=yield createImageBitmap(h);s.postMessage({id:w,bitmap:v,width:h.width,height:h.height,dataURLOptions:o.dataURLOptions},[v])})),p=requestAnimationFrame(f)};p=requestAnimationFrame(f),this.resetObservers=()=>{a(),cancelAnimationFrame(p)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Vt(t,r,n,!1),o=fn(this.processMutation.bind(this),t,r,n),a=pn(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>an(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class bn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Br,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:ft(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class wn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Te()})}let D,Pe,Ze,Fe=!1;const q=hr();function Me(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:c=null,inlineStylesheet:u=!0,maskAllInputs:p,maskInputOptions:m,slimDOMOptions:f,maskInputFn:g,maskTextFn:h,hooks:y,packFn:w,sampling:v={},dataURLOptions:S={},mousemoveWait:b,recordDOM:M=!0,recordCanvas:F=!1,recordCrossOriginIframes:P=!1,recordAfter:k=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:T=!1,collectFonts:j=!1,inlineImages:V=!1,plugins:x,keepIframeSrcFn:oe=()=>!1,ignoreCSSAttributes:H=new Set([]),errorHandler:ee}=e;$r(ee);const K=P?window.parent===window:!0;let $e=!1;if(!K)try{window.parent.document&&($e=!1)}catch{$e=!0}if(K&&!t)throw new Error("emit function is required");b!==void 0&&v.mousemove===void 0&&(v.mousemove=b),q.reset();const at=p===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},lt=f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:f==="all",headMetaDescKeywords:f==="all"}:f||{};Fr();let nr,ct=0;const ir=I=>{for(const X of x||[])X.eventProcessor&&(I=X.eventProcessor(I));return w&&!$e&&(I=w(I)),I};D=(I,X)=>{var N;if(!((N=re[0])===null||N===void 0)&&N.isFrozen()&&I.type!==_.FullSnapshot&&!(I.type===_.IncrementalSnapshot&&I.data.source===C.Mutation)&&re.forEach(z=>z.unfreeze()),K)t?.(ir(I),X);else if($e){const z={type:"rrweb",event:ir(I),origin:window.location.origin,isCheckout:X};window.parent.postMessage(z,"*")}if(I.type===_.FullSnapshot)nr=I,ct=0;else if(I.type===_.IncrementalSnapshot){if(I.data.source===C.Mutation&&I.data.isAttachIframe)return;ct++;const z=n&&ct>=n,le=r&&I.timestamp-nr.timestamp>r;(z||le)&&Pe(!0)}};const Ge=I=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Mutation},I)}))},or=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Scroll},I)})),sr=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},I)})),Un=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},I)})),se=new bn({mutationCb:Ge,adoptedStyleSheetCb:Un}),ae=new on({mirror:q,mutationCb:Ge,stylesheetManager:se,recordCrossOriginIframes:P,wrappedEmit:D});for(const I of x||[])I.getMirror&&I.getMirror({nodeMirror:q,crossOriginIframeMirror:ae.crossOriginIframeMirror,crossOriginIframeStyleMirror:ae.crossOriginIframeStyleMirror});const ut=new wn;Ze=new vn({recordCanvas:F,mutationCb:sr,win:window,blockClass:i,blockSelector:o,mirror:q,sampling:v.canvas,dataURLOptions:S});const je=new sn({mutationCb:Ge,scrollCb:or,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskInputOptions:at,dataURLOptions:S,maskTextFn:h,maskInputFn:g,recordCanvas:F,inlineImages:V,sampling:v,slimDOMOptions:lt,iframeManager:ae,stylesheetManager:se,canvasManager:Ze,keepIframeSrcFn:oe,processedNodeManager:ut},mirror:q});Pe=(I=!1)=>{if(!M)return;D(L({type:_.Meta,data:{href:window.location.href,width:Ct(),height:It()}}),I),se.reset(),je.init(),re.forEach(N=>N.lock());const X=Lr(document,{mirror:q,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskAllInputs:at,maskTextFn:h,slimDOM:lt,dataURLOptions:S,recordCanvas:F,inlineImages:V,onSerialize:N=>{xt(N,q)&&ae.addIframe(N),Et(N,q)&&se.trackLinkElement(N),Ye(N)&&je.addShadowRoot(N.shadowRoot,document)},onIframeLoad:(N,z)=>{ae.attachIframe(N,z),je.observeAttachShadow(N)},onStylesheetLoad:(N,z)=>{se.attachLinkElement(N,z)},keepIframeSrcFn:oe});if(!X)return console.warn("Failed to snapshot the document");D(L({type:_.FullSnapshot,data:{node:X,initialOffset:Mt(window)}}),I),re.forEach(N=>N.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&se.adoptStyleSheets(document.adoptedStyleSheets,q.getId(document))};try{const I=[],X=z=>{var le;return O(nn)({mutationCb:Ge,mousemoveCb:(R,dt)=>D(L({type:_.IncrementalSnapshot,data:{source:dt,positions:R}})),mouseInteractionCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},R)})),scrollCb:or,viewportResizeCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},R)})),inputCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Input},R)})),mediaInteractionCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},R)})),styleSheetRuleCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},R)})),styleDeclarationCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},R)})),canvasMutationCb:sr,fontCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Font},R)})),selectionCb:R=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Selection},R)}))},customElementCb:R=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},R)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:c,maskInputOptions:at,inlineStylesheet:u,sampling:v,recordDOM:M,recordCanvas:F,inlineImages:V,userTriggeredOnInput:T,collectFonts:j,doc:z,maskInputFn:g,maskTextFn:h,keepIframeSrcFn:oe,blockSelector:o,slimDOMOptions:lt,dataURLOptions:S,mirror:q,iframeManager:ae,stylesheetManager:se,shadowDomManager:je,processedNodeManager:ut,canvasManager:Ze,ignoreCSSAttributes:H,plugins:((le=x?.filter(R=>R.observer))===null||le===void 0?void 0:le.map(R=>({observer:R.observer,options:R.options,callback:dt=>D(L({type:_.Plugin,data:{plugin:R.name,payload:dt}}))})))||[]},y)};ae.addLoadListener(z=>{try{I.push(X(z.contentDocument))}catch(le){console.warn(le)}});const N=()=>{Pe(),I.push(X(document)),Fe=!0};return document.readyState==="interactive"||document.readyState==="complete"?N():(I.push(W("DOMContentLoaded",()=>{D(L({type:_.DomContentLoaded,data:{}})),k==="DOMContentLoaded"&&N()})),I.push(W("load",()=>{D(L({type:_.Load,data:{}})),k==="load"&&N()},window))),()=>{I.forEach(z=>z()),ut.destroy(),Fe=!1,Gr()}}catch(I){console.warn(I)}}Me.addCustomEvent=(e,t)=>{if(!Fe)throw new Error("please add custom event after start recording");D(L({type:_.Custom,data:{tag:e,payload:t}}))},Me.freezePage=()=>{re.forEach(e=>e.freeze())},Me.takeFullSnapshot=e=>{if(!Fe)throw new Error("please take full snapshot after start recording");Pe(e)},Me.mirror=q;var Mn={DEBUG:!1,LIB_VERSION:"2.53.0"},B;if(typeof window>"u"){var Jt={hostname:""};B={navigator:{userAgent:""},document:{location:Jt,referrer:""},screen:{width:0,height:0},location:Jt}}else B=window;var Be=24*60*60*1e3,We=Array.prototype,In=Function.prototype,Xt=Object.prototype,ne=We.slice,Ie=Xt.toString,Ue=Xt.hasOwnProperty,Ce=B.console,Oe=B.navigator,G=B.document,He=B.opera,ze=B.screen,ie=Oe.userAgent,et=In.bind,Kt=We.forEach,Yt=We.indexOf,Qt=We.map,Cn=Array.isArray,tt={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},J={log:function(){},warn:function(){},error:function(){},critical:function(){if(!d.isUndefined(Ce)&&Ce){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{Ce.error.apply(Ce,e)}catch{d.each(e,function(r){Ce.error(r)})}}}},rt=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(J,arguments)}},On=function(e){return{log:rt(J.log,e),error:rt(J.error,e),critical:rt(J.critical,e)}};d.bind=function(e,t){var r,n;if(et&&e.bind===et)return et.apply(e,ne.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=ne.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(ne.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(ne.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(Kt&&e.forEach===Kt)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,c="",u="",p=0,m=a,f=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,f=[],Ie.apply(g)==="[object Array]"){for(p=g.length,s=0;s="0"&&t<="9";)g+=t,o();if(t===".")for(g+=".";o()&&t>="0"&&t<="9";)g+=t;if(t==="e"||t==="E")for(g+=t,o(),(t==="-"||t==="+")&&(g+=t,o());t>="0"&&t<="9";)g+=t,o();if(f=+g,!isFinite(f))i("Bad number");else return f},l=function(){var f,g,h="",y;if(t==='"')for(;o();){if(t==='"')return o(),h;if(t==="\\")if(o(),t==="u"){for(y=0,g=0;g<4&&(f=parseInt(o(),16),!!isFinite(f));g+=1)y=y*16+f;h+=String.fromCharCode(y)}else if(typeof r[t]=="string")h+=r[t];else break;else h+=t}i("Bad string")},s=function(){for(;t&&t<=" ";)o()},c=function(){switch(t){case"t":return o("t"),o("r"),o("u"),o("e"),!0;case"f":return o("f"),o("a"),o("l"),o("s"),o("e"),!1;case"n":return o("n"),o("u"),o("l"),o("l"),null}i('Unexpected "'+t+'"')},u,p=function(){var f=[];if(t==="["){if(o("["),s(),t==="]")return o("]"),f;for(;t;){if(f.push(u()),s(),t==="]")return o("]"),f;o(","),s()}}i("Bad array")},m=function(){var f,g={};if(t==="{"){if(o("{"),s(),t==="}")return o("}"),g;for(;t;){if(f=l(),s(),o(":"),Object.hasOwnProperty.call(g,f)&&i('Duplicate key "'+f+'"'),g[f]=u(),s(),t==="}")return o("}"),g;o(","),s()}}i("Bad object")};return u=function(){switch(s(),t){case"{":return m();case"[":return p();case'"':return l();case"-":return a();default:return t>="0"&&t<="9"?a():c()}},function(f){var g;return n=f,e=0,t=" ",g=u(),s(),t&&i("Syntax error"),g}}(),d.base64Encode=function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r,n,i,o,a,l,s,c,u=0,p=0,m="",f=[];if(!e)return e;e=d.utf8Encode(e);do r=e.charCodeAt(u++),n=e.charCodeAt(u++),i=e.charCodeAt(u++),c=r<<16|n<<8|i,o=c>>18&63,a=c>>12&63,l=c>>6&63,s=c&63,f[p++]=t.charAt(o)+t.charAt(a)+t.charAt(l)+t.charAt(s);while(u127&&a<2048?l=String.fromCharCode(a>>6|192,a&63|128):l=String.fromCharCode(a>>12|224,a>>6&63|128,a&63|128),l!==null&&(n>r&&(t+=e.substring(r,n)),t+=l,r=n=o+1)}return n>r&&(t+=e.substring(r,e.length)),t},d.UUID=function(){var e=function(){var n=1*new Date,i;if(B.performance&&B.performance.now)i=B.performance.now();else for(i=0;n==1*new Date;)i++;return n.toString(16)+Math.floor(i).toString(16)},t=function(){return Math.random().toString(16).replace(".","")},r=function(){var n=ie,i,o,a=[],l=0;function s(c,u){var p,m=0;for(p=0;p=4&&(l=s(l,a),a=[]);return a.length>0&&(l=s(l,a)),l.toString(16)};return function(){var n=(ze.height*ze.width).toString(16);return e()+"-"+t()+"-"+r()+"-"+n+"-"+e()}}();var Zt=["ahrefsbot","ahrefssiteaudit","baiduspider","bingbot","bingpreview","chrome-lighthouse","facebookexternal","petalbot","pinterest","screaming frog","yahoo! slurp","yandexbot","adsbot-google","apis-google","duplexweb-google","feedfetcher-google","google favicon","google web preview","google-read-aloud","googlebot","googleweblight","mediapartners-google","storebot-google"];d.isBlockedUA=function(e){var t;for(e=e.toLowerCase(),t=0;t=0}function n(i){if(!G.getElementsByTagName)return[];var o=i.split(" "),a,l,s,c,u,p,m,f,g,h,y=[G];for(p=0;p-1){l=a.split("#"),s=l[0];var w=l[1],v=G.getElementById(w);if(!v||s&&v.nodeName.toLowerCase()!=s)return[];y=[v];continue}if(a.indexOf(".")>-1){l=a.split("."),s=l[0];var S=l[1];for(s||(s="*"),c=[],u=0,m=0;m-1};break;default:k=function(T){return T.getAttribute(M)}}for(y=[],h=0,m=0;m=3?t[2]:""},currentUrl:function(){return B.location.href},properties:function(e){return typeof e!="object"&&(e={}),d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ie,Oe.vendor,He),$referrer:G.referrer,$referring_domain:d.info.referringDomain(G.referrer),$device:d.info.device(ie)}),{$current_url:d.info.currentUrl(),$browser_version:d.info.browserVersion(ie,Oe.vendor,He),$screen_height:ze.height,$screen_width:ze.width,mp_lib:"web",$lib_version:Mn.LIB_VERSION,$insert_id:er(),time:d.timestamp()/1e3},d.strip_empty_properties(e))},people_properties:function(){return d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ie,Oe.vendor,He)}),{$browser_version:d.info.browserVersion(ie,Oe.vendor,He)})},mpPageViewProperties:function(){return d.strip_empty_properties({current_page_title:G.title,current_domain:B.location.hostname,current_url_path:B.location.pathname,current_url_protocol:B.location.protocol,current_url_search:B.location.search})}};var er=function(e){var t=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return e?t.substring(0,e):t},Tn=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Rn=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,tr=function(e){var t=Rn,r=e.split("."),n=r[r.length-1];(n.length>4||n==="com"||n==="org")&&(t=Tn);var i=e.match(t);return i?i[0]:""},it=null,ot=null;typeof JSON<"u"&&(it=JSON.stringify,ot=JSON.parse),it=it||d.JSONEncode,ot=ot||d.JSONDecode,d.toArray=d.toArray,d.isObject=d.isObject,d.JSONEncode=d.JSONEncode,d.JSONDecode=d.JSONDecode,d.isBlockedUA=d.isBlockedUA,d.isEmptyObject=d.isEmptyObject,d.info=d.info,d.info.device=d.info.device,d.info.browser=d.info.browser,d.info.browserVersion=d.info.browserVersion,d.info.properties=d.info.properties;var Nn="__mp_opt_in_out_";function Dn(e,t){if(Bn(t))return J.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),!0;var r=Fn(e,t)==="0";return r&&J.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."),r}function An(e){return Wn(e,function(t){return this.get_config(t)})}function Ln(e){return e=e||{},e.persistenceType==="localStorage"?d.localStorage:d.cookie}function Pn(e,t){return t=t||{},(t.persistencePrefix||Nn)+e}function Fn(e,t){return Ln(t).get(Pn(e,t))}function Bn(e){if(e&&e.ignoreDnt)return!1;var t=e&&e.window||B,r=t.navigator||{},n=!1;return d.each([r.doNotTrack,r.msDoNotTrack,t.doNotTrack],function(i){d.includes([!0,1,"1","yes"],i)&&(n=!0)}),n}function Wn(e,t){return function(){var r=!1;try{var n=t.call(this,"token"),i=t.call(this,"ignore_dnt"),o=t.call(this,"opt_out_tracking_persistence_type"),a=t.call(this,"opt_out_tracking_cookie_prefix"),l=t.call(this,"window");n&&(r=Dn(n,{ignoreDnt:i,persistenceType:o,persistencePrefix:a,window:l}))}catch(c){J.error("Unexpected error when checking tracking opt-out status: "+c)}if(!r)return e.apply(this,arguments);var s=arguments[arguments.length-1];typeof s=="function"&&s(0)}}var st=On("recorder"),rr=window.CompressionStream,Q=function(e){this._mixpanel=e,this._stopRecording=null,this.recEvents=[],this.seqNo=0,this.replayId=null,this.replayStartTime=null,this.batchStartTime=null,this.replayLengthMs=0,this.sendBatchId=null,this.idleTimeoutId=null,this.maxTimeoutId=null,this.recordMaxMs=Be};Q.prototype.get_config=function(e){return this._mixpanel.get_config(e)},Q.prototype.startRecording=function(){if(this._stopRecording!==null){st.log("Recording already in progress, skipping startRecording.");return}this.recordMaxMs=this.get_config("record_max_ms"),this.recordMaxMs>Be&&(this.recordMaxMs=Be,st.critical("record_max_ms cannot be greater than "+Be+"ms. Capping value.")),this.recEvents=[],this.seqNo=0,this.startDate=new Date,this.replayStartTime=this.startDate.getTime(),this.batchStartTime=this.replayStartTime,this.replayId=d.UUID(),this.replayLengthMs=0;var e=d.bind(function(){clearTimeout(this.idleTimeoutId),this.idleTimeoutId=setTimeout(d.bind(function(){st.log("Idle timeout reached, restarting recording."),this.resetRecording()},this),this.get_config("record_idle_timeout_ms"))},this);this._stopRecording=Me({emit:d.bind(function(t){this.recEvents.push(t),this.replayLengthMs=new Date().getTime()-this.replayStartTime,e()},this),maskAllInputs:!0,maskTextSelector:this.get_config("record_mask_text_selector"),blockSelector:this.get_config("record_block_selector"),maskTextClass:this.get_config("record_mask_text_class"),blockClass:this.get_config("record_block_class")}),e(),this.sendBatchId=setInterval(d.bind(this.flushEventsWithOptOut,this),1e4),this.maxTimeoutId=setTimeout(d.bind(this.resetRecording,this),this.recordMaxMs)},Q.prototype.resetRecording=function(){this.stopRecording(),this.startRecording()},Q.prototype.stopRecording=function(){this._stopRecording!==null&&(this._stopRecording(),this._stopRecording=null),this._flushEvents(),this.replayId=null,clearInterval(this.sendBatchId),clearTimeout(this.idleTimeoutId),clearTimeout(this.maxTimeoutId)},Q.prototype.flushEventsWithOptOut=function(){this._flushEvents(d.bind(this._onOptOut,this))},Q.prototype._onOptOut=function(e){e===0&&(this.recEvents=[],this.stopRecording())},Q.prototype._sendRequest=function(e,t){window.fetch(this.get_config("api_host")+"/"+this.get_config("api_routes").record+"?"+new URLSearchParams(e),{method:"POST",headers:{Authorization:"Basic "+btoa(this.get_config("token")+":"),"Content-Type":"application/octet-stream"},body:t})},Q.prototype._flushEvents=An(function(){var e=this.recEvents.length;if(e>0){var t={distinct_id:String(this._mixpanel.get_distinct_id()),seq:this.seqNo++,batch_start_time:this.batchStartTime/1e3,replay_id:this.replayId,replay_length_ms:this.replayLengthMs,replay_start_time:this.replayStartTime/1e3},r=d.JSONEncode(this.recEvents),n=this._mixpanel.get_property("$device_id");n&&(t.$device_id=n);var i=this._mixpanel.get_property("$user_id");if(i&&(t.$user_id=i),this.recEvents=this.recEvents.slice(e),this.batchStartTime=new Date().getTime(),rr){var o=new Blob([r],{type:"application/json"}).stream(),a=o.pipeThrough(new rr("gzip"));new Response(a).blob().then(d.bind(function(l){t.format="gzip",this._sendRequest(t,l)},this))}else t.format="body",this._sendRequest(t,r)}}),window.__mp_recorder=Q})(); +======= + ***************************************************************************** */function e(u,c,h,m){function f(g){return g instanceof h?g:new h(function(p){p(g)})}return new(h||(h=Promise))(function(g,p){function y(v){try{S(m.next(v))}catch(b){p(b)}}function w(v){try{S(m.throw(v))}catch(b){p(b)}}function S(v){v.done?g(v.value):f(v.value).then(y,w)}S((m=m.apply(u,c||[])).next())})}for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=typeof Uint8Array>"u"?[]:new Uint8Array(256),n=0;n>2],f+=t[(c[h]&3)<<4|c[h+1]>>4],f+=t[(c[h+1]&15)<<2|c[h+2]>>6],f+=t[c[h+2]&63];return m%3===2?f=f.substring(0,f.length-1)+"=":m%3===1&&(f=f.substring(0,f.length-2)+"=="),f};const o=new Map,a=new Map;function l(u,c,h){return e(this,void 0,void 0,function*(){const m=`${u}-${c}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const f=new OffscreenCanvas(u,c);f.getContext("2d");const p=yield(yield f.convertToBlob(h)).arrayBuffer(),y=i(p);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(u){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:c,bitmap:h,width:m,height:f,dataURLOptions:g}=u.data,p=l(m,f,g),y=new OffscreenCanvas(m,f);y.getContext("2d").drawImage(h,0,0),h.close();const S=yield y.convertToBlob(g),v=S.type,b=yield S.arrayBuffer(),I=i(b);if(!o.has(c)&&(yield p)===I)return o.set(c,I),s.postMessage({id:c});if(o.get(c)===I)return s.postMessage({id:c});s.postMessage({id:c,type:v,base64:I,width:m,height:f}),o.set(c,I)}else return s.postMessage({id:u.data.id})})}})()},null);class xn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,u)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(u)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Qt(r,n,i,!0),l=new Map,s=new On;s.onmessage=g=>{const{id:p}=g.data;if(l.set(p,!1),!("base64"in g.data))return;const{base64:y,type:w,width:S,height:v}=g.data;this.mutationCb({id:p,type:ye["2D"],commands:[{property:"clearRect",args:[0,0,S,v]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const u=1e3/t;let c=0,h;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(p=>{H(p,n,i,!0)||g.push(p)}),g},f=g=>{if(c&&g-cyn(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(p);if(l.get(w)||p.width===0||p.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(p.__context)){const v=p.getContext(p.__context);((y=v?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&v.clear(v.COLOR_BUFFER_BIT)}const S=yield createImageBitmap(p);s.postMessage({id:w,bitmap:S,width:p.width,height:p.height,dataURLOptions:o.dataURLOptions},[S])})),h=requestAnimationFrame(f)};h=requestAnimationFrame(f),this.resetObservers=()=>{a(),cancelAnimationFrame(h)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Qt(t,r,n,!1),o=wn(this.processMutation.bind(this),t,r,n),a=Mn(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>gn(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class kn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Vr,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:vt(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class Tn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Ae()})}let N,Ue,at,He=!1;const J=Ir();function Ee(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:u=null,inlineStylesheet:c=!0,maskAllInputs:h,maskInputOptions:m,slimDOMOptions:f,maskInputFn:g,maskTextFn:p,hooks:y,packFn:w,sampling:S={},dataURLOptions:v={},mousemoveWait:b,recordDOM:I=!0,recordCanvas:B=!1,recordCrossOriginIframes:P=!1,recordAfter:k=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:T=!1,collectFonts:G=!1,inlineImages:V=!1,plugins:O,keepIframeSrcFn:le=()=>!1,ignoreCSSAttributes:z=new Set([]),errorHandler:ne}=e;Qr(ne);const Z=P?window.parent===window:!0;let Ye=!1;if(!Z)try{window.parent.document&&(Ye=!1)}catch{Ye=!0}if(Z&&!t)throw new Error("emit function is required");b!==void 0&&S.mousemove===void 0&&(S.mousemove=b),J.reset();const ht=h===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},pt=f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:f==="all",headMetaDescKeywords:f==="all"}:f||{};Gr();let fr,mt=0;const hr=M=>{for(const K of O||[])K.eventProcessor&&(M=K.eventProcessor(M));return w&&!Ye&&(M=w(M)),M};N=(M,K)=>{var D;if(!((D=oe[0])===null||D===void 0)&&D.isFrozen()&&M.type!==E.FullSnapshot&&!(M.type===E.IncrementalSnapshot&&M.data.source===C.Mutation)&&oe.forEach(q=>q.unfreeze()),Z)t?.(hr(M),K);else if(Ye){const q={type:"rrweb",event:hr(M),origin:window.location.origin,isCheckout:K};window.parent.postMessage(q,"*")}if(M.type===E.FullSnapshot)fr=M,mt=0;else if(M.type===E.IncrementalSnapshot){if(M.data.source===C.Mutation&&M.data.isAttachIframe)return;mt++;const q=n&&mt>=n,de=r&&M.timestamp-fr.timestamp>r;(q||de)&&Ue(!0)}};const Qe=M=>{N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Mutation},M)}))},pr=M=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Scroll},M)})),mr=M=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},M)})),Qn=M=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},M)})),ue=new kn({mutationCb:Qe,adoptedStyleSheetCb:Qn}),ce=new pn({mirror:J,mutationCb:Qe,stylesheetManager:ue,recordCrossOriginIframes:P,wrappedEmit:N});for(const M of O||[])M.getMirror&&M.getMirror({nodeMirror:J,crossOriginIframeMirror:ce.crossOriginIframeMirror,crossOriginIframeStyleMirror:ce.crossOriginIframeStyleMirror});const gt=new Tn;at=new xn({recordCanvas:B,mutationCb:mr,win:window,blockClass:i,blockSelector:o,mirror:J,sampling:S.canvas,dataURLOptions:v});const Ze=new mn({mutationCb:Qe,scrollCb:pr,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:u,inlineStylesheet:c,maskInputOptions:ht,dataURLOptions:v,maskTextFn:p,maskInputFn:g,recordCanvas:B,inlineImages:V,sampling:S,slimDOMOptions:pt,iframeManager:ce,stylesheetManager:ue,canvasManager:at,keepIframeSrcFn:le,processedNodeManager:gt},mirror:J});Ue=(M=!1)=>{if(!I)return;N(L({type:E.Meta,data:{href:window.location.href,width:Tt(),height:kt()}}),M),ue.reset(),Ze.init(),oe.forEach(D=>D.lock());const K=$r(document,{mirror:J,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:u,inlineStylesheet:c,maskAllInputs:ht,maskTextFn:p,slimDOM:pt,dataURLOptions:v,recordCanvas:B,inlineImages:V,onSerialize:D=>{Nt(D,J)&&ce.addIframe(D),At(D,J)&&ue.trackLinkElement(D),ot(D)&&Ze.addShadowRoot(D.shadowRoot,document)},onIframeLoad:(D,q)=>{ce.attachIframe(D,q),Ze.observeAttachShadow(D)},onStylesheetLoad:(D,q)=>{ue.attachLinkElement(D,q)},keepIframeSrcFn:le});if(!K)return console.warn("Failed to snapshot the document");N(L({type:E.FullSnapshot,data:{node:K,initialOffset:xt(window)}}),M),oe.forEach(D=>D.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&ue.adoptStyleSheets(document.adoptedStyleSheets,J.getId(document))};try{const M=[],K=q=>{var de;return _(hn)({mutationCb:Qe,mousemoveCb:(R,yt)=>N(L({type:E.IncrementalSnapshot,data:{source:yt,positions:R}})),mouseInteractionCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},R)})),scrollCb:pr,viewportResizeCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},R)})),inputCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Input},R)})),mediaInteractionCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},R)})),styleSheetRuleCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},R)})),styleDeclarationCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},R)})),canvasMutationCb:mr,fontCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Font},R)})),selectionCb:R=>{N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Selection},R)}))},customElementCb:R=>{N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},R)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:u,maskInputOptions:ht,inlineStylesheet:c,sampling:S,recordDOM:I,recordCanvas:B,inlineImages:V,userTriggeredOnInput:T,collectFonts:G,doc:q,maskInputFn:g,maskTextFn:p,keepIframeSrcFn:le,blockSelector:o,slimDOMOptions:pt,dataURLOptions:v,mirror:J,iframeManager:ce,stylesheetManager:ue,shadowDomManager:Ze,processedNodeManager:gt,canvasManager:at,ignoreCSSAttributes:z,plugins:((de=O?.filter(R=>R.observer))===null||de===void 0?void 0:de.map(R=>({observer:R.observer,options:R.options,callback:yt=>N(L({type:E.Plugin,data:{plugin:R.name,payload:yt}}))})))||[]},y)};ce.addLoadListener(q=>{try{M.push(K(q.contentDocument))}catch(de){console.warn(de)}});const D=()=>{Ue(),M.push(K(document)),He=!0};return document.readyState==="interactive"||document.readyState==="complete"?D():(M.push(U("DOMContentLoaded",()=>{N(L({type:E.DomContentLoaded,data:{}})),k==="DOMContentLoaded"&&D()})),M.push(U("load",()=>{N(L({type:E.Load,data:{}})),k==="load"&&D()},window))),()=>{M.forEach(q=>q()),gt.destroy(),He=!1,Zr()}}catch(M){console.warn(M)}}Ee.addCustomEvent=(e,t)=>{if(!He)throw new Error("please add custom event after start recording");N(L({type:E.Custom,data:{tag:e,payload:t}}))},Ee.freezePage=()=>{oe.forEach(e=>e.freeze())},Ee.takeFullSnapshot=e=>{if(!He)throw new Error("please take full snapshot after start recording");Ue(e)},Ee.mirror=J;var er={DEBUG:!0,LIB_VERSION:"2.50.0"},W;if(typeof window>"u"){var tr={hostname:""};W={navigator:{userAgent:""},document:{location:tr,referrer:""},screen:{width:0,height:0},location:tr}}else W=window;var ze=24*60*60*1e3,qe=Array.prototype,Rn=Function.prototype,rr=Object.prototype,se=qe.slice,Oe=rr.toString,$e=rr.hasOwnProperty,F=W.console,xe=W.navigator,j=W.document,je=W.opera,Ge=W.screen,ae=xe.userAgent,lt=Rn.bind,nr=qe.forEach,ir=qe.indexOf,or=qe.map,Dn=Array.isArray,ut={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},X={log:function(){if(!d.isUndefined(F)&&F)try{F.log.apply(F,arguments)}catch{d.each(arguments,function(t){F.log(t)})}},warn:function(){if(!d.isUndefined(F)&&F){var e=["Mixpanel warning:"].concat(d.toArray(arguments));try{F.warn.apply(F,e)}catch{d.each(e,function(r){F.warn(r)})}}},error:function(){if(!d.isUndefined(F)&&F){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{F.error.apply(F,e)}catch{d.each(e,function(r){F.error(r)})}}},critical:function(){if(!d.isUndefined(F)&&F){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{F.error.apply(F,e)}catch{d.each(e,function(r){F.error(r)})}}}},ct=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(X,arguments)}},Ve=function(e){return{log:ct(X.log,e),error:ct(X.error,e),critical:ct(X.critical,e)}};d.bind=function(e,t){var r,n;if(lt&&e.bind===lt)return lt.apply(e,se.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=se.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(se.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(se.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(nr&&e.forEach===nr)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,u="",c="",h=0,m=a,f=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,f=[],Oe.apply(g)==="[object Array]"){for(h=g.length,s=0;s="0"&&t<="9";)g+=t,o();if(t===".")for(g+=".";o()&&t>="0"&&t<="9";)g+=t;if(t==="e"||t==="E")for(g+=t,o(),(t==="-"||t==="+")&&(g+=t,o());t>="0"&&t<="9";)g+=t,o();if(f=+g,!isFinite(f))i("Bad number");else return f},l=function(){var f,g,p="",y;if(t==='"')for(;o();){if(t==='"')return o(),p;if(t==="\\")if(o(),t==="u"){for(y=0,g=0;g<4&&(f=parseInt(o(),16),!!isFinite(f));g+=1)y=y*16+f;p+=String.fromCharCode(y)}else if(typeof r[t]=="string")p+=r[t];else break;else p+=t}i("Bad string")},s=function(){for(;t&&t<=" ";)o()},u=function(){switch(t){case"t":return o("t"),o("r"),o("u"),o("e"),!0;case"f":return o("f"),o("a"),o("l"),o("s"),o("e"),!1;case"n":return o("n"),o("u"),o("l"),o("l"),null}i('Unexpected "'+t+'"')},c,h=function(){var f=[];if(t==="["){if(o("["),s(),t==="]")return o("]"),f;for(;t;){if(f.push(c()),s(),t==="]")return o("]"),f;o(","),s()}}i("Bad array")},m=function(){var f,g={};if(t==="{"){if(o("{"),s(),t==="}")return o("}"),g;for(;t;){if(f=l(),s(),o(":"),Object.hasOwnProperty.call(g,f)&&i('Duplicate key "'+f+'"'),g[f]=c(),s(),t==="}")return o("}"),g;o(","),s()}}i("Bad object")};return c=function(){switch(s(),t){case"{":return m();case"[":return h();case'"':return l();case"-":return a();default:return t>="0"&&t<="9"?a():u()}},function(f){var g;return n=f,e=0,t=" ",g=c(),s(),t&&i("Syntax error"),g}}(),d.base64Encode=function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r,n,i,o,a,l,s,u,c=0,h=0,m="",f=[];if(!e)return e;e=d.utf8Encode(e);do r=e.charCodeAt(c++),n=e.charCodeAt(c++),i=e.charCodeAt(c++),u=r<<16|n<<8|i,o=u>>18&63,a=u>>12&63,l=u>>6&63,s=u&63,f[h++]=t.charAt(o)+t.charAt(a)+t.charAt(l)+t.charAt(s);while(c127&&a<2048?l=String.fromCharCode(a>>6|192,a&63|128):l=String.fromCharCode(a>>12|224,a>>6&63|128,a&63|128),l!==null&&(n>r&&(t+=e.substring(r,n)),t+=l,r=n=o+1)}return n>r&&(t+=e.substring(r,e.length)),t},d.UUID=function(){var e=function(){var n=1*new Date,i;if(W.performance&&W.performance.now)i=W.performance.now();else for(i=0;n==1*new Date;)i++;return n.toString(16)+Math.floor(i).toString(16)},t=function(){return Math.random().toString(16).replace(".","")},r=function(){var n=ae,i,o,a=[],l=0;function s(u,c){var h,m=0;for(h=0;h=4&&(l=s(l,a),a=[]);return a.length>0&&(l=s(l,a)),l.toString(16)};return function(){var n=(Ge.height*Ge.width).toString(16);return e()+"-"+t()+"-"+r()+"-"+n+"-"+e()}}();var sr=["ahrefsbot","ahrefssiteaudit","baiduspider","bingbot","bingpreview","chrome-lighthouse","facebookexternal","petalbot","pinterest","screaming frog","yahoo! slurp","yandexbot","adsbot-google","apis-google","duplexweb-google","feedfetcher-google","google favicon","google web preview","google-read-aloud","googlebot","googleweblight","mediapartners-google","storebot-google"];d.isBlockedUA=function(e){var t;for(e=e.toLowerCase(),t=0;t=0}function n(i){if(!j.getElementsByTagName)return[];var o=i.split(" "),a,l,s,u,c,h,m,f,g,p,y=[j];for(h=0;h-1){l=a.split("#"),s=l[0];var w=l[1],S=j.getElementById(w);if(!S||s&&S.nodeName.toLowerCase()!=s)return[];y=[S];continue}if(a.indexOf(".")>-1){l=a.split("."),s=l[0];var v=l[1];for(s||(s="*"),u=[],c=0,m=0;m-1};break;default:k=function(T){return T.getAttribute(I)}}for(y=[],p=0,m=0;m=3?t[2]:""},currentUrl:function(){return W.location.href},properties:function(e){return typeof e!="object"&&(e={}),d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ae,xe.vendor,je),$referrer:j.referrer,$referring_domain:d.info.referringDomain(j.referrer),$device:d.info.device(ae)}),{$current_url:d.info.currentUrl(),$browser_version:d.info.browserVersion(ae,xe.vendor,je),$screen_height:Ge.height,$screen_width:Ge.width,mp_lib:"web",$lib_version:er.LIB_VERSION,$insert_id:ft(),time:d.timestamp()/1e3},d.strip_empty_properties(e))},people_properties:function(){return d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ae,xe.vendor,je)}),{$browser_version:d.info.browserVersion(ae,xe.vendor,je)})},mpPageViewProperties:function(){return d.strip_empty_properties({current_page_title:j.title,current_domain:W.location.hostname,current_url_path:W.location.pathname,current_url_protocol:W.location.protocol,current_url_search:W.location.search})}};var ft=function(e){var t=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return e?t.substring(0,e):t},Fn=function(e){var t=new XMLHttpRequest;if(t.open(e.method,e.url,!0),d.each(e.headers,function(n,i){t.setRequestHeader(i,n)}),e.timeout_ms&&typeof t.timeout<"u"){t.timeout=e.timeout_ms;var r=new Date().getTime()}t.withCredentials=!0,t.onreadystatechange=function(){if(t.readyState===4)if(t.status===200){if(e.callback)if(e.verbose){var n;try{n=d.JSONDecode(t.responseText)}catch(o){if(e.report_error(o),e.ignore_json_errors)n=t.responseText;else return}e.callback(n)}else e.callback(Number(t.responseText))}else{var i;t.timeout&&!t.status&&new Date().getTime()-r>=t.timeout?i="timeout":i="Bad HTTP status: "+t.status+" "+t.statusText,e.report_error(i),e.callback&&(e.verbose?e.callback({status:0,error:i,xhr_req:t}):e.callback(0))}},t.send(e.body_data)},Pn=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Bn=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,ar=function(e){var t=Bn,r=e.split("."),n=r[r.length-1];(n.length>4||n==="com"||n==="org")&&(t=Pn);var i=e.match(t);return i?i[0]:""},Xe=null,Ke=null;typeof JSON<"u"&&(Xe=JSON.stringify,Ke=JSON.parse),Xe=Xe||d.JSONEncode,Ke=Ke||d.JSONDecode,d.toArray=d.toArray,d.isObject=d.isObject,d.JSONEncode=d.JSONEncode,d.JSONDecode=d.JSONDecode,d.isBlockedUA=d.isBlockedUA,d.isEmptyObject=d.isEmptyObject,d.info=d.info,d.info.device=d.info.device,d.info.browser=d.info.browser,d.info.browserVersion=d.info.browserVersion,d.info.properties=d.info.properties;var Wn="__mp_opt_in_out_";function Un(e,t){if(jn(t))return X.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),!0;var r=$n(e,t)==="0";return r&&X.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."),r}function Hn(e){return Gn(e,function(t){return this.get_config(t)})}function zn(e){return e=e||{},e.persistenceType==="localStorage"?d.localStorage:d.cookie}function qn(e,t){return t=t||{},(t.persistencePrefix||Wn)+e}function $n(e,t){return zn(t).get(qn(e,t))}function jn(e){if(e&&e.ignoreDnt)return!1;var t=e&&e.window||W,r=t.navigator||{},n=!1;return d.each([r.doNotTrack,r.msDoNotTrack,t.doNotTrack],function(i){d.includes([!0,1,"1","yes"],i)&&(n=!0)}),n}function Gn(e,t){return function(){var r=!1;try{var n=t.call(this,"token"),i=t.call(this,"ignore_dnt"),o=t.call(this,"opt_out_tracking_persistence_type"),a=t.call(this,"opt_out_tracking_cookie_prefix"),l=t.call(this,"window");n&&(r=Un(n,{ignoreDnt:i,persistenceType:o,persistencePrefix:a,window:l}))}catch(u){X.error("Unexpected error when checking tracking opt-out status: "+u)}if(!r)return e.apply(this,arguments);var s=arguments[arguments.length-1];typeof s=="function"&&s(0)}}var Vn=Ve("lock"),lr=function(e,t){t=t||{},this.storageKey=e,this.storage=t.storage||window.localStorage,this.pollIntervalMS=t.pollIntervalMS||100,this.timeoutMS=t.timeoutMS||2e3};lr.prototype.withLock=function(e,t,r){!r&&typeof t!="function"&&(r=t,t=null);var n=r||new Date().getTime()+"|"+Math.random(),i=new Date().getTime(),o=this.storageKey,a=this.pollIntervalMS,l=this.timeoutMS,s=this.storage,u=o+":X",c=o+":Y",h=o+":Z",m=function(S){t&&t(S)},f=function(S){if(new Date().getTime()-i>l){Vn.error("Timeout waiting for mutex on "+o+"; clearing lock. ["+n+"]"),s.removeItem(h),s.removeItem(c),y();return}setTimeout(function(){try{S()}catch(v){m(v)}},a*(Math.random()+.1))},g=function(S,v){S()?v():f(function(){g(S,v)})},p=function(){var S=s.getItem(c);if(S&&S!==n)return!1;if(s.setItem(c,n),s.getItem(c)===n)return!0;if(!Je(s,!0))throw new Error("localStorage support dropped while acquiring lock");return!1},y=function(){s.setItem(u,n),g(p,function(){if(s.getItem(u)===n){w();return}f(function(){if(s.getItem(c)!==n){y();return}g(function(){return!s.getItem(h)},w)})})},w=function(){s.setItem(h,"1");try{e()}finally{s.removeItem(h),s.getItem(c)===n&&s.removeItem(c),s.getItem(u)===n&&s.removeItem(u)}};try{if(Je(s,!0))y();else throw new Error("localStorage support check failed")}catch(S){m(S)}};var ur=Ve("batch"),re=function(e,t){t=t||{},this.storageKey=e,this.storage=t.storage||window.localStorage,this.reportError=t.errorReporter||d.bind(ur.error,ur),this.lock=new lr(e,{storage:this.storage}),this.usePersistence=t.usePersistence,this.pid=t.pid||null,this.memQueue=[]};re.prototype.enqueue=function(e,t,r){var n={id:ft(),flushAfter:new Date().getTime()+t*2,payload:e};if(!this.usePersistence){this.memQueue.push(n),r&&r(!0);return}this.lock.withLock(d.bind(function(){var o;try{var a=this.readFromStorage();a.push(n),o=this.saveToStorage(a),o&&this.memQueue.push(n)}catch{this.reportError("Error enqueueing item",e),o=!1}r&&r(o)},this),d.bind(function(o){this.reportError("Error acquiring storage lock",o),r&&r(!1)},this),this.pid)},re.prototype.fillBatch=function(e){var t=this.memQueue.slice(0,e);if(this.usePersistence&&t.lengtho.flushAfter&&!n[o.id]&&(o.orphaned=!0,t.push(o),t.length>=e))break}}}return t};var cr=function(e,t){var r=[];return d.each(e,function(n){n.id&&!t[n.id]&&r.push(n)}),r};re.prototype.removeItemsByID=function(e,t){var r={};if(d.each(e,function(i){r[i]=!0}),this.memQueue=cr(this.memQueue,r),!this.usePersistence){t&&t(!0);return}var n=d.bind(function(){var i;try{var o=this.readFromStorage();if(o=cr(o,r),i=this.saveToStorage(o),i){o=this.readFromStorage();for(var a=0;a5&&(this.reportError("[dupe] item ID sent too many times, not sending",{item:u,batchSize:i.length,timesSent:this.itemIdsSentSuccessfully[m]}),h=!1):this.reportError("[dupe] found item with no ID",{item:u}),h&&o.push(c)}a[u.id]=c},this),o.length<1){this.resetFlush();return}this.requestInProgress=!0;var l=d.bind(function(u){this.requestInProgress=!1;try{var c=!1;if(e.unloading)this.queue.updatePayloads(a);else if(d.isObject(u)&&u.error==="timeout"&&new Date().getTime()-r>=t)this.reportError("Network timeout; retrying"),this.flush();else if(d.isObject(u)&&u.xhr_req&&(u.xhr_req.status>=500||u.xhr_req.status===429||u.error==="timeout")){var h=this.flushInterval*2,m=u.xhr_req.responseHeaders;if(m){var f=m["Retry-After"];f&&(h=parseInt(f,10)*1e3||h)}h=Math.min(Jn,h),this.reportError("Error; retry in "+h+" ms"),this.scheduleFlush(h)}else if(d.isObject(u)&&u.xhr_req&&u.xhr_req.status===413)if(i.length>1){var g=Math.max(1,Math.floor(n/2));this.batchSize=Math.min(this.batchSize,g,i.length-1),this.reportError("413 response; reducing batch size to "+this.batchSize),this.resetFlush()}else this.reportError("Single-event request too large; dropping",i),this.resetBatchSize(),c=!0;else c=!0;c&&(this.queue.removeItemsByID(d.map(i,function(p){return p.id}),d.bind(function(p){p?(this.consecutiveRemovalFailures=0,this.forceDelayFlush?this.resetFlush():this.flush()):(this.reportError("Failed to remove items from queue"),++this.consecutiveRemovalFailures>5?(this.reportError("Too many queue failures; disabling batching system."),this.options.stopAllBatchingFunc()):this.resetFlush())},this)),d.each(i,d.bind(function(p){var y=p.id;y?(this.itemIdsSentSuccessfully[y]=this.itemIdsSentSuccessfully[y]||0,this.itemIdsSentSuccessfully[y]++,this.itemIdsSentSuccessfully[y]>5&&this.reportError("[dupe] item ID sent too many times",{item:p,batchSize:i.length,timesSent:this.itemIdsSentSuccessfully[y]})):this.reportError("[dupe] found item with no ID while removing",{item:p})},this)))}catch(p){this.reportError("Error handling API response",p),this.resetFlush()}},this),s={method:"POST",verbose:!0,ignore_json_errors:!0,timeout_ms:t};e.unloading&&(s.transport="sendBeacon"),ke.log("MIXPANEL REQUEST:",o),this.options.sendRequestFunc(o,s,l)}catch(u){this.reportError("Error flushing request queue",u),this.resetFlush()}},Y.prototype.reportError=function(e,t){if(ke.error.apply(ke.error,arguments),this.options.errorReporter)try{t instanceof Error||(t=new Error(e)),this.options.errorReporter(e,t)}catch(r){ke.error(r)}};var Se=Ve("recorder"),Xn=1e3,Kn=10*1e3,Yn=90*1e3,Q=function(e){this._mixpanel=e,this._stopRecording=null,this.recEvents=[],this.seqNo=0,this.replayId=null,this.replayStartTime=null,this.batchStartTime=null,this.replayLengthMs=0,this.sendBatchId=null,this.idleTimeoutId=null,this.maxTimeoutId=null,this.recordMaxMs=ze,this._initBatcher()};Q.prototype._initBatcher=function(){this.batcher=new Y("__mprec",{batchSize:Xn,flushIntervalMs:Kn,requestTimeoutMs:Yn,autoStart:!0,sendRequestFunc:d.bind(function(e,t,r){this.sendRequestWithOptOut(e,t,r)},this),forceDelayFlush:!0})},Q.prototype.get_config=function(e){return this._mixpanel.get_config(e)},Q.prototype.startRecording=function(){if(this._stopRecording!==null){Se.log("Recording already in progress, skipping startRecording.");return}this.recordMaxMs=this.get_config("record_max_ms"),this.recordMaxMs>ze&&(this.recordMaxMs=ze,Se.critical("record_max_ms cannot be greater than "+ze+"ms. Capping value.")),this.recEvents=[],this.seqNo=0,this.startDate=new Date,this.replayStartTime=this.startDate.getTime(),this.batchStartTime=this.replayStartTime,this.replayId=d.UUID(),this.replayLengthMs=0,this.batcher.start();var e=d.bind(function(){clearTimeout(this.idleTimeoutId),this.idleTimeoutId=setTimeout(d.bind(function(){Se.log("Idle timeout reached, restarting recording."),this.resetRecording()},this),this.get_config("record_idle_timeout_ms"))},this);this._stopRecording=Ee({emit:d.bind(function(t){this.batcher.enqueue(t),this.replayLengthMs=new Date().getTime()-this.replayStartTime,e()},this),maskAllInputs:!0,maskTextSelector:this.get_config("record_mask_text_selector")}),e(),this.maxTimeoutId=setTimeout(d.bind(this.resetRecording,this),this.recordMaxMs)},Q.prototype.resetRecording=function(){this.stopRecording(),this.startRecording()},Q.prototype.stopRecording=function(){this._stopRecording!==null&&(this._stopRecording(),this._stopRecording=null),this.batcher.flush(),this.replayId=null,clearTimeout(this.idleTimeoutId),clearTimeout(this.maxTimeoutId)},Q.prototype.sendRequestWithOptOut=function(e,t,r){this._sendRequest(e,t,r,d.bind(this._onOptOut,this))},Q.prototype._onOptOut=function(e){e===0&&(this.recEvents=[],this.stopRecording())},Q.prototype._sendRequest=Hn(function(e,t,r){var n=this.get_config("api_host")+"/"+this.get_config("api_routes").record,i={Authorization:"Basic "+btoa(this.get_config("token")+":"),"Content-Type":"application/json"},o={distinct_id:String(this._mixpanel.get_distinct_id()),events:e,seq:this.seqNo++,batch_start_time:this.batchStartTime/1e3,replay_id:this.replayId,replay_length_ms:this.replayLengthMs,replay_start_time:this.replayStartTime/1e3},a=this._mixpanel.get_property("$device_id");a&&(o.$device_id=a);var l=this._mixpanel.get_property("$user_id");l&&(o.$user_id=l);var s=d.JSONEncode(o),u=d.extend({},t,{method:"POST",url:n,body_data:s,headers:i,callback:r,reportError:d.bind(this.reportError,this)});Fn(u)}),Q.prototype.reportError=function(e,t){Se.error.apply(Se.error,arguments);try{!t&&!(e instanceof Error)&&(e=new Error(e)),this.get_config("error_reporter")(e,t)}catch(r){Se.error(r)}},window.__mp_recorder=Q})(); +>>>>>>> 571d8b5 (cleanup, draft) diff --git a/dist/mixpanel.amd.js b/dist/mixpanel.amd.js index f00fce89..e0072480 100644 --- a/dist/mixpanel.amd.js +++ b/dist/mixpanel.amd.js @@ -1,8 +1,13 @@ define((function () { 'use strict'; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1680,6 +1685,83 @@ define((function () { 'use strict'; return maxlen ? guid.substring(0, maxlen) : guid; }; + + /** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ + var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); + }; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2051,6 +2133,7 @@ define((function () { 'use strict'; this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2075,6 +2158,14 @@ define((function () { 'use strict'; 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2100,6 +2191,7 @@ define((function () { 'use strict'; }, this), this.pid); }; + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2108,7 +2200,7 @@ define((function () { 'use strict'; */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2157,7 +2249,13 @@ define((function () { 'use strict'; _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2239,6 +2337,13 @@ define((function () { 'use strict'; */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2300,7 +2405,10 @@ define((function () { 'use strict'; */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2315,22 +2423,23 @@ define((function () { 'use strict'; * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2341,7 +2450,7 @@ define((function () { 'use strict'; * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -2373,26 +2482,26 @@ define((function () { 'use strict'; }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -2415,16 +2524,16 @@ define((function () { 'use strict'; } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2525,12 +2634,16 @@ define((function () { 'use strict'; _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -2572,8 +2685,7 @@ define((function () { 'use strict'; requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2585,12 +2697,12 @@ define((function () { 'use strict'; */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4686,69 +4798,22 @@ define((function () { 'use strict'; } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -4839,7 +4904,10 @@ define((function () { 'use strict'; return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/dist/mixpanel.cjs.js b/dist/mixpanel.cjs.js index 037d365e..a99b854a 100644 --- a/dist/mixpanel.cjs.js +++ b/dist/mixpanel.cjs.js @@ -1,8 +1,13 @@ 'use strict'; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1680,6 +1685,83 @@ var cheap_guid = function(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; + +/** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ +var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); +}; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2051,6 +2133,7 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2075,6 +2158,14 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2100,6 +2191,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { }, this), this.pid); }; + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2108,7 +2200,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2157,7 +2249,13 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2239,6 +2337,13 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2300,7 +2405,10 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2315,22 +2423,23 @@ var logger = console_with_prefix('batch'); * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2341,7 +2450,7 @@ var RequestBatcher = function(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -2373,26 +2482,26 @@ RequestBatcher.prototype.clear = function() { }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -2415,16 +2524,16 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2525,12 +2634,16 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -2572,8 +2685,7 @@ RequestBatcher.prototype.flush = function(options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2585,12 +2697,12 @@ RequestBatcher.prototype.flush = function(options) { */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4686,69 +4798,22 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -4839,7 +4904,10 @@ MixpanelLib.prototype.init_batchers = function() { return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/dist/mixpanel.globals.js b/dist/mixpanel.globals.js index 979211bf..3f2b0f39 100644 --- a/dist/mixpanel.globals.js +++ b/dist/mixpanel.globals.js @@ -2,8 +2,13 @@ 'use strict'; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1681,6 +1686,83 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; + + /** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ + var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); + }; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2052,6 +2134,7 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2076,6 +2159,14 @@ 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2101,6 +2192,7 @@ }, this), this.pid); }; + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2109,7 +2201,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2158,7 +2250,13 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2240,6 +2338,13 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2301,7 +2406,10 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2316,22 +2424,23 @@ * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2342,7 +2451,7 @@ * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -2374,26 +2483,26 @@ }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -2416,16 +2525,16 @@ } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2526,12 +2635,16 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -2573,8 +2686,7 @@ requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2586,12 +2698,12 @@ */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4687,69 +4799,22 @@ } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -4840,7 +4905,10 @@ return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/dist/mixpanel.min.js b/dist/mixpanel.min.js index bf19d8f6..2257c868 100644 --- a/dist/mixpanel.min.js +++ b/dist/mixpanel.min.js @@ -1,4 +1,5 @@ (function() { +<<<<<<< HEAD var l=void 0,m=!0,r=null,D=!1; (function(){function Aa(){function a(){if(!a.Dc)la=a.Dc=m,ma=D,c.a(F,function(a){a.qc()})}function b(){try{t.documentElement.doScroll("left")}catch(d){setTimeout(b,1);return}a()}if(t.addEventListener)"complete"===t.readyState?a():t.addEventListener("DOMContentLoaded",a,D);else if(t.attachEvent){t.attachEvent("onreadystatechange",a);var d=D;try{d=n.frameElement===r}catch(f){}t.documentElement.doScroll&&d&&b()}c.Tb(n,"load",a,m)}function Ba(){x.init=function(a,b,d){if(d)return x[d]||(x[d]=F[d]=S(a, b,d),x[d].ka()),x[d];d=x;if(F.mixpanel)d=F.mixpanel;else if(a)d=S(a,b,"mixpanel"),d.ka(),F.mixpanel=d;x=d;1===ca&&(n.mixpanel=x);Ca()}}function Ca(){c.a(F,function(a,b){"mixpanel"!==b&&(x[b]=a)});x._=c}function da(a){a=c.e(a)?a:c.g(a)?{}:{days:a};return c.extend({},Da,a)}function S(a,b,d){var f,h="mixpanel"===d?x:x[d];if(h&&0===ca)f=h;else{if(h&&!c.isArray(h)){o.error("You have already initialized "+d);return}f=new e}f.kb={};f.X(a,b,d);f.people=new j;f.people.X(f);if(!f.c("skip_first_touch_marketing")){var a= @@ -109,4 +110,116 @@ e.prototype.s;e.prototype.get_distinct_id=e.prototype.L;e.prototype.toString=e.p e.prototype.ud;e.prototype.start_batch_senders=e.prototype.ab;e.prototype.stop_batch_senders=e.prototype.bb;e.prototype.start_session_recording=e.prototype.dc;e.prototype.stop_session_recording=e.prototype.ld;e.prototype.get_session_recording_properties=e.prototype.Db;e.prototype.DEFAULT_API_ROUTES=A;q.prototype.properties=q.prototype.aa;q.prototype.update_search_keyword=q.prototype.lc;q.prototype.update_referrer_info=q.prototype.gb;q.prototype.get_cross_subdomain=q.prototype.Gc;q.prototype.clear= q.prototype.clear;var F={};(function(){ca=1;x=n.mixpanel;c.g(x)?o.A('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.'):x.__loaded||x.config&&x.persistence?o.A("The Mixpanel library has already been downloaded at least once. Ensure that the Mixpanel code snippet only appears once on the page (and is not double-loaded by a tag manager) in order to avoid errors."):1.1>(x.__SV||0)?o.A("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."): (c.a(x._i,function(a){a&&c.isArray(a)&&(F[a[a.length-1]]=S.apply(this,a))}),Ba(),x.init(),c.a(F,function(a){a.ka()}),Aa())})()})(); +======= +var l=void 0,m=!0,q=null,D=!1; +(function(){function Aa(){function a(){if(!a.Jc)la=a.Jc=m,ma=D,c.c(F,function(a){a.uc()})}function b(){try{t.documentElement.doScroll("left")}catch(d){setTimeout(b,1);return}a()}if(t.addEventListener)"complete"===t.readyState?a():t.addEventListener("DOMContentLoaded",a,D);else if(t.attachEvent){t.attachEvent("onreadystatechange",a);var d=D;try{d=j.frameElement===q}catch(f){}t.documentElement.doScroll&&d&&b()}c.Zb(j,"load",a,m)}function Ba(){x.init=function(a,b,d){if(d)return x[d]||(x[d]=F[d]=S(a, +b,d),x[d].ka()),x[d];d=x;if(F.mixpanel)d=F.mixpanel;else if(a)d=S(a,b,"mixpanel"),d.ka(),F.mixpanel=d;x=d;1===ca&&(j.mixpanel=x);Ca()}}function Ca(){c.c(F,function(a,b){"mixpanel"!==b&&(x[b]=a)});x._=c}function da(a){a=c.e(a)?a:c.g(a)?{}:{days:a};return c.extend({},Da,a)}function S(a,b,d){var f,g="mixpanel"===d?x:x[d];if(g&&0===ca)f=g;else{if(g&&!c.isArray(g)){p.error("You have already initialized "+d);return}f=new e}f.nb={};f.Y(a,b,d);f.people=new n;f.people.Y(f);if(!f.a("skip_first_touch_marketing")){var a= +c.info.Z(q),h={},u=D;c.c(a,function(a,b){(h["initial_"+b]=a)&&(u=m)});u&&f.people.N(h)}J=J||f.a("debug");!c.g(g)&&c.isArray(g)&&(f.Ba.call(f.people,g.people),f.Ba(g));return f}function e(){}function P(){}function Ea(a){return a}function o(a){this.props={};this.Ed=D;this.name=a.persistence_name?"mp_"+a.persistence_name:"mp_"+a.token+"_mixpanel";var b=a.persistence;if("cookie"!==b&&"localStorage"!==b)p.A("Unknown persistence type "+b+"; falling back to cookie"),b=a.persistence="cookie";this.i="localStorage"=== +b&&c.localStorage.ra()?c.localStorage:c.cookie;this.load();this.pc(a);this.Ad(a);this.save()}function n(){}function v(){}function C(a,b){this.options=b;this.ca=new G(a,{oa:c.bind(this.h,this),i:b.i,C:b.C});this.Db=this.options.F;this.Ma=this.options.Hb;this.Ib=b.Ib||D;this.ua=!this.options.Cc;this.Ka=0;this.H={}}function na(a,b){var d=[];c.c(a,function(a){var c=a.id;if(c in b){if(c=b[c],c!==q)a.payload=c,d.push(a)}else d.push(a)});return d}function oa(a,b){var d=[];c.c(a,function(a){a.id&&!b[a.id]&& +d.push(a)});return d}function G(a,b){b=b||{};this.O=a;this.i=b.i||window.localStorage;this.h=b.oa||c.bind(pa.error,pa);this.$a=new qa(a,{i:this.i});this.C=b.C;this.ta=b.ta||q;this.B=[]}function qa(a,b){b=b||{};this.O=a;this.i=b.i||window.localStorage;this.Xb=b.Xb||100;this.kc=b.kc||2E3}function T(){this.Ub="submit"}function M(){this.Ub="click"}function E(){}function ra(a){var b=Fa,d=a.split("."),d=d[d.length-1];if(4=b.timeout?"timeout":"Bad HTTP status: "+ +b.status+" "+b.statusText,a.l(f),a.J&&(a.xa?a.J({status:0,error:f,S:b}):a.J(0))};b.send(a.Ec)}function ea(a){var b=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return a?b.substring(0,a):b}function U(a,b){if(fa!==q&&!b)return fa;var d=m;try{var a=a||window.localStorage,c="__mplss_"+ea(8);a.setItem(c,"xyz");"xyz"!==a.getItem(c)&&(d=D);a.removeItem(c)}catch(g){d=D}return fa=d}function ga(a){return{log:ha(p.log,a),error:ha(p.error,a),A:ha(p.A,a)}}function ha(a, +b){return function(){arguments[0]="["+b+"] "+arguments[0];return a.apply(p,arguments)}}function Ia(a,b){sa(m,a,b)}function Ja(a,b){sa(D,a,b)}function Ka(a,b){return"1"===V(b).get(W(a,b))}function ta(a,b){if(La(b))return p.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),m;var d="0"===V(b).get(W(a,b));d&&p.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."); +return d}function K(a){return ia(a,function(a){return this.a(a)})}function H(a){return ia(a,function(a){return this.p(a)})}function N(a){return ia(a,function(a){return this.p(a)})}function Ma(a,b){b=b||{};V(b).remove(W(a,b),!!b.Bb,b.zb)}function V(a){a=a||{};return"localStorage"===a.Wb?c.localStorage:c.cookie}function W(a,b){b=b||{};return(b.Vb||Na)+a}function La(a){if(a&&a.Lb)return D;var a=a&&a.window||j,b=a.navigator||{},d=D;c.c([b.doNotTrack,b.msDoNotTrack,a.doNotTrack],function(a){c.j([m,1,"1", +"yes"],a)&&(d=m)});return d}function sa(a,b,d){!c.Za(b)||!b.length?p.error("gdpr."+(a?"optIn":"optOut")+" called with an invalid token"):(d=d||{},V(d).set(W(b,d),a?1:0,c.Pb(d.Ab)?d.Ab:q,!!d.Bb,!!d.dd,!!d.Gc,d.zb),d.o&&a&&d.o(d.td||"$opt_in",d.ud,{send_immediately:m}))}function ia(a,b){return function(){var d=D;try{var c=b.call(this,"token"),g=b.call(this,"ignore_dnt"),h=b.call(this,"opt_out_tracking_persistence_type"),u=b.call(this,"opt_out_tracking_cookie_prefix"),i=b.call(this,"window");c&&(d=ta(c, +{Lb:g,Wb:h,Vb:u,window:i}))}catch(e){p.error("Unexpected error when checking tracking opt-out status: "+e)}if(!d)return a.apply(this,arguments);d=arguments[arguments.length-1];"function"===typeof d&&d(0)}}var J=m,j;if("undefined"===typeof window){var A={hostname:""};j={navigator:{userAgent:""},document:{location:A,referrer:""},screen:{width:0,height:0},location:A}}else j=window;var A=Array.prototype,ua=Object.prototype,L=A.slice,Q=ua.toString,X=ua.hasOwnProperty,y=j.console,I=j.navigator,t=j.document, +Y=j.opera,Z=j.screen,z=I.userAgent,ja=Function.prototype.bind,va=A.forEach,wa=A.indexOf,xa=A.map,A=Array.isArray,ka={},c={trim:function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},p={log:function(){if(J&&!c.g(y)&&y)try{y.log.apply(y,arguments)}catch(a){c.c(arguments,function(a){y.log(a)})}},warn:function(){if(J&&!c.g(y)&&y){var a=["Mixpanel warning:"].concat(c.P(arguments));try{y.warn.apply(y,a)}catch(b){c.c(a,function(a){y.warn(a)})}}},error:function(){if(J&&!c.g(y)&&y){var a= +["Mixpanel error:"].concat(c.P(arguments));try{y.error.apply(y,a)}catch(b){c.c(a,function(a){y.error(a)})}}},A:function(){if(!c.g(y)&&y){var a=["Mixpanel error:"].concat(c.P(arguments));try{y.error.apply(y,a)}catch(b){c.c(a,function(a){y.error(a)})}}}};c.bind=function(a,b){var d,f;if(ja&&a.bind===ja)return ja.apply(a,L.call(arguments,1));if(!c.Ya(a))throw new TypeError;d=L.call(arguments,2);return f=function(){if(!(this instanceof f))return a.apply(b,d.concat(L.call(arguments)));var c={};c.prototype= +a.prototype;var h=new c;c.prototype=q;c=a.apply(h,d.concat(L.call(arguments)));return Object(c)===c?c:h}};c.c=function(a,b,d){if(!(a===q||a===l))if(va&&a.forEach===va)a.forEach(b,d);else if(a.length===+a.length)for(var c=0,g=a.length;ca?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())};c.fa=function(a){var b= +{};c.c(a,function(a,f){c.Za(a)&&0=i;)g()}function d(){var a,b,d="",c;if('"'===i)for(;g();){if('"'===i)return g(),d;if("\\"===i)if(g(),"u"===i){for(b=c=0;4>b;b+=1){a=parseInt(g(),16);if(!isFinite(a))break;c=16*c+a}d+=String.fromCharCode(c)}else if("string"===typeof k[i])d+=k[i];else break;else d+=i}h("Bad string")}function c(){var a;a="";"-"===i&&(a="-",g("-"));for(;"0"<=i&&"9">=i;)a+=i,g();if("."===i)for(a+=".";g()&&"0"<=i&&"9">=i;)a+=i;if("e"===i||"E"===i){a+=i;g();if("-"===i||"+"===i)a+=i,g();for(;"0"<=i&&"9">=i;)a+=i,g()}a= ++a;if(isFinite(a))return a;h("Bad number")}function g(a){a&&a!==i&&h("Expected '"+a+"' instead of '"+i+"'");i=s.charAt(e);e+=1;return i}function h(a){a=new SyntaxError(a);a.Dd=e;a.text=s;throw a;}var e,i,k={'"':'"',"\\":"\\","/":"/",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"},s,r;r=function(){b();switch(i){case "{":var e;a:{var u,k={};if("{"===i){g("{");b();if("}"===i){g("}");e=k;break a}for(;i;){u=d();b();g(":");Object.hasOwnProperty.call(k,u)&&h('Duplicate key "'+u+'"');k[u]=r();b();if("}"===i){g("}"); +e=k;break a}g(",");b()}}h("Bad object")}return e;case "[":a:{e=[];if("["===i){g("[");b();if("]"===i){g("]");u=e;break a}for(;i;){e.push(r());b();if("]"===i){g("]");u=e;break a}g(",");b()}}h("Bad array")}return u;case '"':return d();case "-":return c();default:return"0"<=i&&"9">=i?c():a()}};return function(a){s=a;e=0;i=" ";a=r();b();i&&h("Syntax error");return a}}();c.Dc=function(a){var b,d,f,g,h=0,e=0,i="",i=[];if(!a)return a;a=c.Bd(a);do b=a.charCodeAt(h++),d=a.charCodeAt(h++),f=a.charCodeAt(h++), +g=b<<16|d<<8|f,b=g>>18&63,d=g>>12&63,f=g>>6&63,g&=63,i[e++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(d)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(g);while(he?c++:i=127e?String.fromCharCode(e>>6|192,e&63|128):String.fromCharCode(e>>12|224,e>>6&63|128,e&63|128);i!==q&&(c>d&&(b+=a.substring(d,c)),b+=i,d=c=h+1)}c>d&&(b+=a.substring(d,a.length));return b};c.lb=function(){function a(){function a(b,d){var c,f=0;for(c=0;cn?(Ra.error("Timeout waiting for mutex on "+r+"; clearing lock. ["+k+"]"),j.removeItem(t),j.removeItem(p),g()):setTimeout(function(){try{a()}catch(c){b&&b(c)}},w*(Math.random()+0.1))}!c&&"function"!==typeof b&&(c=b,b=q);var k=c|| +(new Date).getTime()+"|"+Math.random(),s=(new Date).getTime(),r=this.O,w=this.Xb,n=this.kc,j=this.i,o=r+":X",p=r+":Y",t=r+":Z";try{if(U(j,m))g();else throw Error("localStorage support check failed");}catch(v){b&&b(v)}};var pa=ga("batch");G.prototype.Pa=function(a,b,d){var f={id:ea(),flushAfter:(new Date).getTime()+2*b,payload:a};this.C?this.$a.kb(c.bind(function(){var b;try{var c=this.ea();c.push(f);(b=this.cb(c))&&this.B.push(f)}catch(e){this.h("Error enqueueing item",a),b=D}d&&d(b)},this),c.bind(function(a){this.h("Error acquiring storage lock", +a);d&&d(D)},this),this.ta):(this.B.push(f),d&&d(m))};G.prototype.Kc=function(a){var b=this.B.slice(0,a);if(this.C&&b.lengthh.flushAfter&&!f[h.id]&&(h.Wc=m,b.push(h),b.length>=a))break}}}return b};G.prototype.Yc=function(a,b){var d={};c.c(a,function(a){d[a]=m});this.B=oa(this.B,d);if(this.C){var f=c.bind(function(){var b;try{var c=this.ea(),c=oa(c,d);if(b=this.cb(c))for(var c= +this.ea(),f=0;fe.length)this.M();else{this.ac=m;var i=c.bind(function(e){this.ac=D;try{var h=D;if(a.nc)this.ca.zd(u);else if(c.e(e)&&"timeout"===e.error&&(new Date).getTime()-d>=b)this.h("Network timeout; retrying"),this.flush();else if(c.e(e)&&e.S&&(500<=e.S.status||429===e.S.status||"timeout"===e.error)){var i=2*this.Gd,k=e.S.responseHeaders;if(k){var j=k["Retry-After"];j&&(i=1E3*parseInt(j,10)||i)}i=Math.min(6E5,i);this.h("Error; retry in "+ +i+" ms");this.cc(i)}else if(c.e(e)&&e.S&&413===e.S.status)if(1(x.__SV||0)?p.A("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."):(c.c(x._i,function(a){a&&c.isArray(a)&&(F[a[a.length-1]]=S.apply(this,a))}),Ba(),x.init(),c.c(F,function(a){a.ka()}),Aa())})()})(); +>>>>>>> 571d8b5 (cleanup, draft) })(); diff --git a/dist/mixpanel.umd.js b/dist/mixpanel.umd.js index f8a611b1..7f6083ef 100644 --- a/dist/mixpanel.umd.js +++ b/dist/mixpanel.umd.js @@ -5,8 +5,13 @@ })(this, (function () { 'use strict'; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1684,6 +1689,83 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; + + /** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ + var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); + }; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2055,6 +2137,7 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2079,6 +2162,14 @@ 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2104,6 +2195,7 @@ }, this), this.pid); }; + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2112,7 +2204,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2161,7 +2253,13 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2243,6 +2341,13 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2304,7 +2409,10 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2319,22 +2427,23 @@ * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2345,7 +2454,7 @@ * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -2377,26 +2486,26 @@ }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -2419,16 +2528,16 @@ } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2529,12 +2638,16 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -2576,8 +2689,7 @@ requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2589,12 +2701,12 @@ */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4690,69 +4802,22 @@ } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -4843,7 +4908,10 @@ return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/examples/commonjs-browserify/bundle.js b/examples/commonjs-browserify/bundle.js index 1ce5fc3f..927c65ea 100644 --- a/examples/commonjs-browserify/bundle.js +++ b/examples/commonjs-browserify/bundle.js @@ -2,8 +2,13 @@ 'use strict'; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1681,6 +1686,83 @@ var cheap_guid = function(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; + +/** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ +var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); +}; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2052,6 +2134,7 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2076,6 +2159,14 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2101,6 +2192,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { }, this), this.pid); }; + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2109,7 +2201,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2158,7 +2250,13 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2240,6 +2338,13 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2301,7 +2406,10 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2316,22 +2424,23 @@ var logger = console_with_prefix('batch'); * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2342,7 +2451,7 @@ var RequestBatcher = function(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -2374,26 +2483,26 @@ RequestBatcher.prototype.clear = function() { }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -2416,16 +2525,16 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2526,12 +2635,16 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -2573,8 +2686,7 @@ RequestBatcher.prototype.flush = function(options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2586,12 +2698,12 @@ RequestBatcher.prototype.flush = function(options) { */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4687,69 +4799,22 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -4840,7 +4905,10 @@ MixpanelLib.prototype.init_batchers = function() { return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/examples/es2015-babelify/bundle.js b/examples/es2015-babelify/bundle.js index eaa0a212..5a5e2aa3 100644 --- a/examples/es2015-babelify/bundle.js +++ b/examples/es2015-babelify/bundle.js @@ -162,8 +162,13 @@ Object.defineProperty(exports, '__esModule', { value: true }); var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; exports['default'] = Config; @@ -1305,66 +1310,22 @@ MixpanelLib.prototype._send_request = function (url, data, options, callback) { } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _utils._.each(headers, function (headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { - // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _utils._.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if (req.timeout && !req.status && new Date().getTime() - start_time >= req.timeout) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({ status: 0, error: error, xhr_req: req }); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + (0, _utils.make_xhr_request)({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -1456,7 +1417,10 @@ MixpanelLib.prototype.init_batchers = function () { if (!this.are_batchers_initialized()) { var batcher_for = _utils._.bind(function (attrs) { return new _requestBatcher.RequestBatcher(attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _utils._.bind(function (data, options, cb) { this._send_request(this.get_config('api_host') + attrs.endpoint, this._encode_data_for_request(data), options, this._prepare_callback(cb, data)); }, this), @@ -4062,22 +4026,23 @@ var logger = (0, _utils.console_with_prefix)('batch'); * @constructor */ var RequestBatcher = function RequestBatcher(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new _requestQueue.RequestQueue(storageKey, { errorReporter: _utils._.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -4088,7 +4053,7 @@ var RequestBatcher = function RequestBatcher(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function (item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -4120,27 +4085,27 @@ RequestBatcher.prototype.clear = function () { }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function () { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function () { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function (flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_utils._.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_utils._.bind(this.flush, this), this.currentFlushInterval); } }; @@ -4163,16 +4128,16 @@ RequestBatcher.prototype.flush = function (options) { } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _utils._.each(batch, function (item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -4261,12 +4226,16 @@ RequestBatcher.prototype.flush = function (options) { }), _utils._.bind(function (succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -4306,7 +4275,7 @@ RequestBatcher.prototype.flush = function (options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch (err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -4318,12 +4287,12 @@ RequestBatcher.prototype.flush = function (options) { */ RequestBatcher.prototype.reportError = function (msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch (err) { logger.error(err); } @@ -4370,6 +4339,7 @@ var RequestQueue = function RequestQueue(storageKey, options) { this.reportError = options.errorReporter || _utils._.bind(logger.error, logger); this.lock = new _sharedLock.SharedLock(storageKey, { storage: this.storage }); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -4394,6 +4364,14 @@ RequestQueue.prototype.enqueue = function (item, flushInterval, cb) { 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_utils._.bind(function lockAcquired() { var succeeded; try { @@ -4427,7 +4405,7 @@ RequestQueue.prototype.enqueue = function (item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function (batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -4480,6 +4458,12 @@ RequestQueue.prototype.removeItemsByID = function (ids, cb) { }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } var removeFromStorage = _utils._.bind(function () { var succeeded; @@ -4562,6 +4546,13 @@ var updatePayloads = function updatePayloads(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function (itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_utils._.bind(function lockAcquired() { var succeeded; try { @@ -4623,7 +4614,10 @@ RequestQueue.prototype.saveToStorage = function (queue) { */ RequestQueue.prototype.clear = function () { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; exports.RequestQueue = RequestQueue; @@ -6452,6 +6446,79 @@ var cheap_guid = function cheap_guid(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; +/** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ +var make_xhr_request = function make_xhr_request(options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function (headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { + // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if (req.timeout && !req.status && new Date().getTime() - start_time >= req.timeout) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({ status: 0, error: error, xhr_req: req }); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); +}; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -6516,5 +6583,6 @@ exports.localStorageSupported = localStorageSupported; exports.JSONStringify = JSONStringify; exports.JSONParse = JSONParse; exports.slice = slice; +exports.make_xhr_request = make_xhr_request; },{"./config":3}]},{},[1]); diff --git a/examples/umd-webpack/bundle.js b/examples/umd-webpack/bundle.js index 942e026a..702bc626 100644 --- a/examples/umd-webpack/bundle.js +++ b/examples/umd-webpack/bundle.js @@ -68,8 +68,13 @@ })(this, (function () { 'use strict'; var Config = { +<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' +======= + DEBUG: true, + LIB_VERSION: '2.50.0' +>>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1747,6 +1752,83 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; + + /** + * Makes an XMLHttpRequest with the given options. + * + * @param {Object} options - Configuration options for the request. + * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). + * @param {string} options.url - The URL to which the request is sent. + * @param {Object} [options.headers] - Additional headers to include in the request. + * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. + * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. + * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. + * @param {Function} [options.callback] - The callback function to execute when the request completes. + * @param {Function} [options.report_error] - The function to execute when an error occurs. + * @param {string|Object} [options.body_data] - The data to send with the request, if any. + */ + var make_xhr_request = function (options) { + var req = new XMLHttpRequest(); + req.open(options.method, options.url, true); + + _.each(options.headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (options.callback) { + if (options.verbose) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + options.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + options.callback(response); + } else { + options.callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + options.report_error(error); + if (options.callback) { + if (options.verbose) { + options.callback({status: 0, error: error, xhr_req: req}); + } else { + options.callback(0); + } + } + } + } + }; + req.send(options.body_data); + }; + // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2118,6 +2200,7 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2142,6 +2225,14 @@ 'payload': item }; + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2167,6 +2258,7 @@ }, this), this.pid); }; + /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2175,7 +2267,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2224,7 +2316,13 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2306,6 +2404,13 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + return; + } + this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2367,7 +2472,10 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2382,22 +2490,23 @@ * @constructor */ var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; + this.options = options; + this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; + this.currentBatchSize = this.options.batchSize; + this.currentFlushInterval = this.options.flushIntervalMs; - this.stopped = !this.libConfig['batch_autostart']; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2408,7 +2517,7 @@ * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); + this.queue.enqueue(item, this.currentFlushInterval, cb); }; /** @@ -2440,26 +2549,26 @@ }; /** - * Restore batch size configuration to whatever is set in the main SDK. + * Restore batch size configuration to the originally initialized value */ RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; + this.currentBatchSize = this.options.batchSize; }; /** - * Restore flush interval time configuration to whatever is set in the main SDK. + * Restore flush interval time configuration to the originally initialized value */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + this.scheduleFlush(this.options.flushIntervalMs); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; + this.currentFlushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); } }; @@ -2482,16 +2591,16 @@ } options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; + var currentBatchSize = this.currentBatchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); + if (this.options.beforeSendHook && !item.orphaned) { + payload = this.options.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2592,12 +2701,16 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); + this.options.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -2639,8 +2752,7 @@ requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - + this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2652,12 +2764,12 @@ */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.errorReporter) { + if (this.options.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.errorReporter(msg, err); + this.options.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4753,69 +4865,22 @@ } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); + make_xhr_request({ + method: options.method, + url: url, + headers: headers, + timeout_ms: options.timeout_ms, + verbose_mode: verbose_mode, + ignore_json_errors: options.ignore_json_errors, + callback: callback, + report_error: lib.report_error, + body_data: body_data + }); } catch (e) { lib.report_error(e); succeeded = false; @@ -4906,7 +4971,10 @@ return new RequestBatcher( attrs.queue_key, { - libConfig: this['config'], + batchSize: this.get_config('batch_size'), + flushIntervalMs: this.get_config('batch_flush_interval_ms'), + requestTimeoutMs: this.get_config('batch_request_timeout_ms'), + autoStart: this.get_config('batch_autostart'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index e77027a6..8e2b027d 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -626,9 +626,6 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { } } else if (USE_XHR) { try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; @@ -643,7 +640,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { ignore_json_errors: options.ignore_json_errors, callback: callback, report_error: lib.report_error, - body_data: body_data, + body_data: body_data }); } catch (e) { lib.report_error(e); diff --git a/src/recorder/index.js b/src/recorder/index.js index 75b2e4b9..470802bd 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -42,7 +42,25 @@ MixpanelRecorder.prototype._initBatcher = function () { sendRequestFunc: _.bind(function(data, options, callback) { this.sendRequestWithOptOut(data, options, callback); }, this), + forceDelayFlush: true, }); + + // var flushOnUnload = _.bind(function() { + // if (!this.batcher.stopped) { + // this.batcher.flush({unloading: true}); + // } + // }, this); + + // window.addEventListener('pagehide', function(ev) { + // if (ev['persisted']) { + // flushOnUnload(); + // } + // }); + // window.addEventListener('visibilitychange', function() { + // if (document['visibilityState'] === 'hidden') { + // flushOnUnload(); + // } + // }); }; // eslint-disable-next-line camelcase @@ -189,7 +207,7 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() { } }); -MixpanelRecorder.prototype._sendRequest = function (data, options, callback) { +MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (data, options, callback) { var url = this.get_config('api_host') + '/' + this.get_config('api_routes')['record']; var headers = { 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), @@ -216,15 +234,44 @@ MixpanelRecorder.prototype._sendRequest = function (data, options, callback) { reqBody['$user_id'] = userId; } + var bodyData = _.JSONEncode(reqBody); + + // if (options.transport === 'sendBeacon') { + // // we have access to fetch in this environment due to MutationObserver requirement. + // // use it as a replacement for sendBeacon so that we can set header authorization. + // window['fetch'](url, { + // method: 'POST', + // headers: headers, + // body: bodyData, + // keepalive: true, + // }); + // callback(true); + // } + var reqOptions = _.extend({}, options, { method: 'POST', url: url, - 'body_data': JSON.stringify(reqBody), + 'body_data': bodyData, headers: headers, callback: callback, + reportError: _.bind(this.reportError, this) }); make_xhr_request(reqOptions); +}); + + +MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } }; + window['__mp_recorder'] = MixpanelRecorder; diff --git a/src/request-batcher.js b/src/request-batcher.js index 429fb5d5..9ebe6363 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -19,13 +19,17 @@ var RequestBatcher = function(storageKey, options) { this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), storage: options.storage, - usePersistence: options.usePersistence, + usePersistence: options.usePersistence }); // seed variable batch size + flush interval with configured values this.currentBatchSize = this.options.batchSize; this.currentFlushInterval = this.options.flushIntervalMs; + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; + this.stopped = !this.options.autoStart; this.consecutiveRemovalFailures = 0; @@ -221,7 +225,11 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.forceDelayFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { diff --git a/src/request-queue.js b/src/request-queue.js index c2b428d6..1a62c167 100644 --- a/src/request-queue.js +++ b/src/request-queue.js @@ -51,17 +51,14 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - if (this.usePersistence) { - this._enqueuePersisted(queueEntry, cb); + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } return; } - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } -}; -RequestQueue.prototype._enqueuePersisted = function (queueEntry, cb) { this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -88,7 +85,6 @@ RequestQueue.prototype._enqueuePersisted = function (queueEntry, cb) { }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items From 9d0b57285d1303a15f0ab5941ba6a530889d8382 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 17 May 2024 15:57:13 +0000 Subject: [PATCH 04/48] trailing comma bad --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 63454c0c..8f951d3d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1815,5 +1815,5 @@ export { JSONStringify, JSONParse, slice, - make_xhr_request, + make_xhr_request }; From 8b53454aba8f2801bb78b536d3e5dc01e60cadcb Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 22 May 2024 00:03:51 +0000 Subject: [PATCH 05/48] update batcher tests --- .vscode/launch.json | 23 +++ src/mixpanel-core.js | 3 +- src/request-batcher.js | 10 +- src/request-queue.js | 2 +- tests/unit/request-batcher.js | 274 ++++++++++++++++++---------------- 5 files changed, 179 insertions(+), 133 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..0cc9863e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Mocha (Test single file)", + "type": "node", + "request": "launch", + "env": { + "BABEL_ENV": "test" + }, + "runtimeArgs": [ + "--require", + "babel-core/register", + "${workspaceRoot}/node_modules/.bin/mocha", + "--inspect-brk", + "${relativeFile}", + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "port": 9229 + } + ] +} \ No newline at end of file diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 8e2b027d..d0e6d803 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -748,7 +748,8 @@ MixpanelLib.prototype.init_batchers = function() { return this._run_hook('before_send_' + attrs.type, item); }, this), errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true, } ); }, this); diff --git a/src/request-batcher.js b/src/request-batcher.js index 9ebe6363..8e516d62 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -117,8 +117,8 @@ RequestBatcher.prototype.flush = function(options) { options = options || {}; var timeoutMS = this.options.requestTimeoutMs; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; - var batch = this.queue.fillBatch(currentBatchSize); + var flushBatchSize = this.currentBatchSize; + var batch = this.queue.fillBatch(flushBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -190,7 +190,7 @@ RequestBatcher.prototype.flush = function(options) { (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry - var retryMS = this.flushInterval * 2; + var retryMS = this.currentFlushInterval * 2; var headers = res.xhr_req['responseHeaders']; if (headers) { var retryAfter = headers['Retry-After']; @@ -204,8 +204,8 @@ RequestBatcher.prototype.flush = function(options) { } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { // 413 Payload Too Large if (batch.length > 1) { - var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); - this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + var halvedBatchSize = Math.max(1, Math.floor(flushBatchSize / 2)); + this.currentBatchSize = Math.min(this.currentBatchSize, halvedBatchSize, batch.length - 1); this.reportError('413 response; reducing batch size to ' + this.batchSize); this.resetFlush(); } else { diff --git a/src/request-queue.js b/src/request-queue.js index 1a62c167..afad17f8 100644 --- a/src/request-queue.js +++ b/src/request-queue.js @@ -148,7 +148,7 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { } return; } - + var removeFromStorage = _.bind(function() { var succeeded; try { diff --git a/tests/unit/request-batcher.js b/tests/unit/request-batcher.js index 617e5dcf..3d9b725d 100644 --- a/tests/unit/request-batcher.js +++ b/tests/unit/request-batcher.js @@ -6,7 +6,7 @@ import sinonChai from 'sinon-chai'; chai.use(sinonChai); import { RequestBatcher } from '../../src/request-batcher'; -import {mapValues} from 'lodash'; +import {assign, mapValues} from 'lodash'; const LOCALSTORAGE_KEY = `fake-rb-key`; const START_TIME = 100000; @@ -19,7 +19,7 @@ describe(`RequestBatcher`, function() { let clock = null; function configureBatchSize(batchSize) { - libConfig.batch_size = batchSize; + batcher.options.batchSize = batchSize; batcher.resetBatchSize(); } @@ -29,8 +29,8 @@ describe(`RequestBatcher`, function() { function sendResponse(status, {error, responseHeaders} = {}) { // respond to last request sent - const requestIndex = batcher.sendRequest.args.length - 1; - batcher.sendRequest.args[requestIndex][2]({ + const requestIndex = batcher.options.sendRequestFunc.args.length - 1; + batcher.options.sendRequestFunc.args[requestIndex][2]({ 'xhr_req': { status, responseHeaders, @@ -39,6 +39,22 @@ describe(`RequestBatcher`, function() { }); } + function initBatcher(optionOverrides) { + optionOverrides = optionOverrides || {}; + const defaultOptions = { + sendRequestFunc: sinon.spy(), + storage: localStorage, + usePersistence: true, + flushIntervalMs: DEFAULT_FLUSH_INTERVAL, + batchSize: 50, + autoStart: true, + requestTimeoutMs: REQUEST_TIMEOUT_MS, + }; + + const options = assign({}, defaultOptions, optionOverrides); + batcher = new RequestBatcher(LOCALSTORAGE_KEY, options); + } + beforeEach(function() { if (clock) { clock.restore(); @@ -46,17 +62,7 @@ describe(`RequestBatcher`, function() { clock = sinon.useFakeTimers(START_TIME); localStorage.clear(); - libConfig = { - batch_flush_interval_ms: DEFAULT_FLUSH_INTERVAL, - batch_request_timeout_ms: REQUEST_TIMEOUT_MS, - batch_size: 50, - batch_autostart: true, - }; - batcher = new RequestBatcher(LOCALSTORAGE_KEY, { - libConfig, - sendRequestFunc: sinon.spy(), - storage: localStorage, - }); + initBatcher(); }); afterEach(function() { @@ -71,6 +77,22 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}, function(succeeded) { expect(succeeded).to.be.ok; expect(batcher.queue.memQueue).to.have.lengthOf(1); + expect(getLocalStorageItems()).to.have.lengthOf(1); + const queuedEntry = batcher.queue.memQueue[0]; + expect(queuedEntry).to.eql(getLocalStorageItems()[0]); + expect(queuedEntry.flushAfter).to.be.greaterThan(START_TIME + 5000); + expect(queuedEntry.flushAfter).to.be.lessThan(START_TIME + 15000); + expect(queuedEntry.payload).to.deep.equal({foo: `bar`}); + done(); + }); + }); + + it(`only stores the item in memory when usePersistence=false`, function(done) { + initBatcher({usePersistence: false}); + batcher.enqueue({foo: `bar`}, function(succeeded) { + expect(succeeded).to.be.ok; + expect(batcher.queue.memQueue).to.have.lengthOf(1); + expect(getLocalStorageItems()).to.not.be.ok; const queuedEntry = batcher.queue.memQueue[0]; expect(queuedEntry.flushAfter).to.be.greaterThan(START_TIME + 5000); expect(queuedEntry.flushAfter).to.be.lessThan(START_TIME + 15000); @@ -83,14 +105,14 @@ describe(`RequestBatcher`, function() { describe(`flush`, function() { it(`does not call sendRequest when queue is empty`, function() { batcher.flush(); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; }); it(`calls sendRequest with items to flush`, function() { batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([{foo: `bar`}]); }); it(`removes items from queue on successful response`, function() { @@ -105,12 +127,12 @@ describe(`RequestBatcher`, function() { }); it(`transforms items before sending if a hook function has been provided`, function() { - batcher.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); + batcher.options.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); batcher.enqueue({Hello: `World`}); batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ {Hello: `WORLD`}, {foo: `BAR`}, ]); @@ -124,48 +146,48 @@ describe(`RequestBatcher`, function() { batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; sendResponse(200); // second request should follow immediately - expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; sendResponse(200); - expect(batcher.sendRequest).to.have.been.calledThrice; // forsooth + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; // forsooth // check what was sent in those requests - expect(batcher.sendRequest.args[0][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ {ev: `queued event 1`}, {ev: `queued event 2`}, {ev: `queued event 3`}, ]); - expect(batcher.sendRequest.args[1][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ {ev: `queued event 4`}, {ev: `queued event 5`}, {ev: `queued event 6`}, ]); - expect(batcher.sendRequest.args[2][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ {ev: `queued event 7`}, {ev: `queued event 8`}, ]); // no new requests after that clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.sendRequest).to.have.been.calledThrice; + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; }); it(`prevents reentrant flushes`, function() { batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; batcher.enqueue({foo2: `bar2`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; // no new request + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request sendResponse(200); - expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([{foo: `bar`}]); - expect(batcher.sendRequest.args[1][0]).to.deep.equal([{foo2: `bar2`}]); + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([{foo2: `bar2`}]); }); describe(`error handling`, function() { @@ -173,50 +195,50 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.enqueue({foo2: `bar2`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; sendResponse(500); clock.tick(DEFAULT_FLUSH_INTERVAL); // no new requests, items are still in queue - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL); // retry with same data - expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.sendRequest.args[1][0]).to.deep.equal(batcher.sendRequest.args[0][0]); + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); // oh no, another explosion! sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); // no new requests, items are still in queue - expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); // retry with same data - expect(batcher.sendRequest).to.have.been.calledThrice; - expect(batcher.sendRequest.args[2][0]).to.deep.equal(batcher.sendRequest.args[0][0]); + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; + expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); // do it again, oh the humanity sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 4); // no new requests, items are still in queue - expect(batcher.sendRequest).to.have.been.calledThrice; + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL * 4); // retry with same data - expect(batcher.sendRequest).to.have.callCount(4); - expect(batcher.sendRequest.args[3][0]).to.deep.equal(batcher.sendRequest.args[0][0]); + expect(batcher.options.sendRequestFunc).to.have.callCount(4); + expect(batcher.options.sendRequestFunc.args[3][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); // will the madness ever end? finally the API call succeeds sendResponse(200); clock.tick(DEFAULT_FLUSH_INTERVAL * 100); // a long time - expect(batcher.sendRequest).to.have.callCount(4); // no new requests + expect(batcher.options.sendRequestFunc).to.have.callCount(4); // no new requests expect(batcher.queue.memQueue).to.be.empty; expect(getLocalStorageItems()).to.be.empty; }); @@ -229,7 +251,7 @@ describe(`RequestBatcher`, function() { let tryAfter = DEFAULT_FLUSH_INTERVAL * 2; const TEN_MINUTES = 10 * 60 * 1000; while (tryAfter <= TEN_MINUTES) { - expect(batcher.sendRequest).to.have.callCount(expectedRequests); + expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); sendResponse(503); clock.tick(tryAfter); @@ -237,20 +259,20 @@ describe(`RequestBatcher`, function() { expectedRequests++; } - expect(batcher.sendRequest).to.have.callCount(expectedRequests); + expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); sendResponse(503); clock.tick(TEN_MINUTES - 1); - expect(batcher.sendRequest).to.have.callCount(expectedRequests); // no new request + expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); // no new request clock.tick(1); - expect(batcher.sendRequest).to.have.callCount(++expectedRequests); + expect(batcher.options.sendRequestFunc).to.have.callCount(++expectedRequests); // do it again, exactly 10 minutes til next request - expect(batcher.sendRequest).to.have.callCount(expectedRequests); + expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); sendResponse(503); clock.tick(TEN_MINUTES - 1); - expect(batcher.sendRequest).to.have.callCount(expectedRequests); // no new request + expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); // no new request clock.tick(1); - expect(batcher.sendRequest).to.have.callCount(++expectedRequests); + expect(batcher.options.sendRequestFunc).to.have.callCount(++expectedRequests); }); it(`resets flush interval when request succeeds after backoff`, function() { @@ -260,31 +282,31 @@ describe(`RequestBatcher`, function() { // fail a couple times clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; sendResponse(503); // configuring default flush interval shouldn't affect anything during failure backoff - libConfig.batch_flush_interval_ms = 8000; + batcher.options.flushIntervalMs = 8000; clock.tick(DEFAULT_FLUSH_INTERVAL * 4); - expect(batcher.sendRequest).to.have.been.calledThrice; + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; sendResponse(503); // succeed! clock.tick(DEFAULT_FLUSH_INTERVAL * 8); - expect(batcher.sendRequest).to.have.callCount(4); + expect(batcher.options.sendRequestFunc).to.have.callCount(4); sendResponse(200); // at this point the success response should have reset the interval to the 8000 // configured above batcher.enqueue({ev: `queued event 3`}); clock.tick(7000); - expect(batcher.sendRequest).to.have.callCount(4); // no new request yet + expect(batcher.options.sendRequestFunc).to.have.callCount(4); // no new request yet clock.tick(1000); - expect(batcher.sendRequest).to.have.callCount(5); + expect(batcher.options.sendRequestFunc).to.have.callCount(5); }); it(`can queue up new events while failing requests are retrying`, function() { @@ -294,19 +316,19 @@ describe(`RequestBatcher`, function() { // fail a couple times clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; sendResponse(503); batcher.enqueue({ev: `queued event 3`}); clock.tick(DEFAULT_FLUSH_INTERVAL * 4); - expect(batcher.sendRequest).to.have.callCount(3); + expect(batcher.options.sendRequestFunc).to.have.callCount(3); // should include all events in current retry - expect(batcher.sendRequest.args[2][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ {ev: `queued event 1`}, {ev: `queued event 2`}, {ev: `queued event 3`}, @@ -323,7 +345,7 @@ describe(`RequestBatcher`, function() { sendResponse(400); clock.tick(100000); - expect(batcher.sendRequest).to.have.been.calledOnce; // no new request + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request expect(batcher.queue.memQueue).to.be.empty; expect(getLocalStorageItems()).to.be.empty; }); @@ -338,7 +360,7 @@ describe(`RequestBatcher`, function() { sendResponse(0); clock.tick(100000); - expect(batcher.sendRequest).to.have.been.calledOnce; // no new request + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request expect(batcher.queue.memQueue).to.be.empty; expect(getLocalStorageItems()).to.be.empty; }); @@ -347,19 +369,19 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.enqueue({foo2: `bar2`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; sendResponse(429); clock.tick(DEFAULT_FLUSH_INTERVAL); // no new requests, items are still in queue - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL); // retry with same data - expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.sendRequest.args[1][0]).to.deep.equal(batcher.sendRequest.args[0][0]); + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); }); it(`reduces batch size after 413 Payload Too Large`, function() { @@ -371,13 +393,13 @@ describe(`RequestBatcher`, function() { batcher.flush(); // should have tried to send all 7 items in one go - expect(batcher.sendRequest.args[0][0]).to.have.lengthOf(7); + expect(batcher.options.sendRequestFunc.args[0][0]).to.have.lengthOf(7); sendResponse(413); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledTwice; // no backoff + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; // no backoff // reduced batch size - expect(batcher.sendRequest.args[1][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ {ev: `queued event 1`}, {ev: `queued event 2`}, {ev: `queued event 3`}, @@ -386,9 +408,9 @@ describe(`RequestBatcher`, function() { sendResponse(200); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledThrice; + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; // remaining items from original batch - expect(batcher.sendRequest.args[2][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ {ev: `queued event 5`}, {ev: `queued event 6`}, {ev: `queued event 7`}, @@ -398,17 +420,17 @@ describe(`RequestBatcher`, function() { it(`does not retry single item which produces 413 Payload Too Large`, function() { batcher.enqueue({ev: `bloated item`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; sendResponse(413); clock.tick(240000); - expect(batcher.sendRequest).to.have.been.calledOnce; // no new request + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request // first item should have been dropped, and we resume normal batching batcher.enqueue({ev: `normal item 1`}); batcher.enqueue({ev: `normal item 2`}); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.sendRequest.args[1][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ {ev: `normal item 1`}, {ev: `normal item 2`}, ]); @@ -421,22 +443,22 @@ describe(`RequestBatcher`, function() { sendResponse(503, {responseHeaders: {'Retry-After': `20`}}); clock.tick(10000); - expect(batcher.sendRequest).to.have.been.calledOnce; // no retry yet + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no retry yet clock.tick(10000); - expect(batcher.sendRequest).to.have.been.calledTwice; // 20s have passed + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; // 20s have passed // after success, should reset to configured flush interval sendResponse(200); batcher.enqueue({ev: `queued event 3`}); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledThrice; - expect(batcher.sendRequest.args[2][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; + expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ {ev: `queued event 3`}, ]); }); it(`handles failures to remove items from queue and eventually stops batchers`, function() { - batcher.stopAllBatching = sinon.spy(); + batcher.options.stopAllBatchingFunc = sinon.spy(); batcher.enqueue({foo: `bar`}); batcher.flush(); @@ -456,36 +478,36 @@ describe(`RequestBatcher`, function() { expect(batcher.consecutiveRemovalFailures).to.equal(1); // no immediate flush - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // make the event orphaned so we try to send it again clock.tick(DEFAULT_FLUSH_INTERVAL * 3); - expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(2); // now it will try to send on every flush clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.callCount(3); + expect(batcher.options.sendRequestFunc).to.have.callCount(3); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(3); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.callCount(4); + expect(batcher.options.sendRequestFunc).to.have.callCount(4); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(4); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.callCount(5); + expect(batcher.options.sendRequestFunc).to.have.callCount(5); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(5); - expect(batcher.stopAllBatching).not.to.have.been.called; + expect(batcher.options.stopAllBatchingFunc).not.to.have.been.called; clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.callCount(6); + expect(batcher.options.sendRequestFunc).to.have.callCount(6); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(6); - expect(batcher.stopAllBatching).to.have.been.calledOnce; + expect(batcher.options.stopAllBatchingFunc).to.have.been.calledOnce; }); context(`when request times out`, function() { @@ -507,8 +529,8 @@ describe(`RequestBatcher`, function() { batcher.flush(); clock.tick(REQUEST_TIMEOUT_MS); timeOutRequest(); - expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.sendRequest.args[1][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([{foo: `bar`}]); }); it(`checks clock before treating it as a real timeout`, function() { @@ -517,17 +539,17 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; timeOutRequest(); // no new request; there was no significant time between sending // the original request and getting the "timeout" response - expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // should have been treated like a normal error and backed off clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.sendRequest.args[1][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([{foo: `bar`}]); }); }); }); @@ -537,15 +559,15 @@ describe(`RequestBatcher`, function() { it(`does not flush`, function() { batcher.enqueue({foo: `bar`}); clock.tick(20000); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; }); it(`flushes immediately on start`, function() { batcher.enqueue({foo: `bar`}); clock.tick(20000); batcher.start(); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([{foo: `bar`}]); }); }); @@ -556,16 +578,16 @@ describe(`RequestBatcher`, function() { it(`does not send requests until flush interval`, function() { batcher.enqueue({first: `event`}); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; clock.tick(1000); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; batcher.enqueue({second: `event`}); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; clock.tick(1000); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ {first: `event`}, {second: `event`}, ]); }); @@ -581,10 +603,10 @@ describe(`RequestBatcher`, function() { // kill it localStorage.removeItem(LOCALSTORAGE_KEY); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ {name: `storagetest 1`}, {name: `storagetest 2`}, ]); @@ -603,8 +625,8 @@ describe(`RequestBatcher`, function() { ])); batcher.start(); - expect(batcher.sendRequest).to.have.been.calledOnce; - const batchEvents = batcher.sendRequest.args[0][0]; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + const batchEvents = batcher.options.sendRequestFunc.args[0][0]; expect(batchEvents).to.have.lengthOf(2); expect(batchEvents[0].event).to.equal(`orphaned event 1`); expect(batchEvents[1].event).to.equal(`orphaned event 2`); @@ -625,8 +647,8 @@ describe(`RequestBatcher`, function() { ])); batcher.start(); - expect(batcher.sendRequest).to.have.been.calledOnce; - const batchEvents = batcher.sendRequest.args[0][0]; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + const batchEvents = batcher.options.sendRequestFunc.args[0][0]; expect(batchEvents).to.have.lengthOf(1); expect(batchEvents[0].event).to.equal(`orphaned event 1`); @@ -648,13 +670,13 @@ describe(`RequestBatcher`, function() { batcher.start(); clock.tick(20000); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; // first event becomes orphaned clock.tick(80000); - expect(batcher.sendRequest).to.have.been.calledOnce; - const payload = batcher.sendRequest.args[0][0]; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + const payload = batcher.options.sendRequestFunc.args[0][0]; expect(payload).to.have.lengthOf(1); expect(payload[0]).to.have.property(`event`, `orphaned event 1`); expect(payload[0]).to.have.nested.include({'properties.foo': `bar`}); @@ -664,13 +686,13 @@ describe(`RequestBatcher`, function() { expect(getLocalStorageItems()).to.have.lengthOf(1); clock.tick(20000); - expect(batcher.sendRequest).to.have.been.calledOnce; // no new request + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request // second event becomes orphaned clock.tick(200000); - expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.sendRequest.args[1][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ {'event': `orphaned event 2`}, ]); @@ -680,7 +702,7 @@ describe(`RequestBatcher`, function() { }); it(`does not apply before-send hooks to orphaned items`, function() { - batcher.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); + batcher.options.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify([ {id: `fakeID1`, flushAfter: Date.now() - 60000, payload: { @@ -694,8 +716,8 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.start(); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ {Hello: `WORLD`}, {foo: `BAR`}, {event: `orphaned event 1`}, // did not get uppercased @@ -709,7 +731,7 @@ describe(`RequestBatcher`, function() { it(`ignores and overwrites malformed localStorage entries`, function() { localStorage.setItem(LOCALSTORAGE_KEY, `just some garbage {{{`); batcher.start(); - expect(batcher.sendRequest).not.to.have.been.called; + expect(batcher.options.sendRequestFunc).not.to.have.been.called; // should clear and overwrite garbage localStorage when enqueueing batcher.enqueue({foo: `bar`}); @@ -720,8 +742,8 @@ describe(`RequestBatcher`, function() { ]); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.sendRequest).to.have.been.calledOnce; - expect(batcher.sendRequest.args[0][0]).to.deep.equal([ + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ {foo: `bar`}, {baz: `quux`}, ]); @@ -742,8 +764,8 @@ describe(`RequestBatcher`, function() { expect(JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY))).to.have.lengthOf(3); batcher.start(); - expect(batcher.sendRequest).to.have.been.calledOnce; - const payload = batcher.sendRequest.args[0][0]; + expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + const payload = batcher.options.sendRequestFunc.args[0][0]; expect(payload).to.have.lengthOf(2); expect(payload[0]).to.have.property(`event`, `orphaned event 1`); expect(payload[0]).to.have.nested.include({'properties.foo': `bar`}); From 023d70340aecb072b2f51425f795ffed67704e29 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 22 May 2024 00:14:31 +0000 Subject: [PATCH 06/48] queue tests --- src/request-batcher.js | 6 +++--- tests/unit/request-queue.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/request-batcher.js b/src/request-batcher.js index 8e516d62..541bd9c8 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -23,14 +23,14 @@ var RequestBatcher = function(storageKey, options) { }); // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.currentBatchSize = options.batchSize; + this.currentFlushInterval = options.flushIntervalMs; // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes // as long as the queue is not empty. This is useful for high-volume events like Session Replay. this.forceDelayFlush = options.forceDelayFlush || false; - this.stopped = !this.options.autoStart; + this.stopped = !options.autoStart; this.consecutiveRemovalFailures = 0; // extra client-side dedupe diff --git a/tests/unit/request-queue.js b/tests/unit/request-queue.js index b535e1c3..cf66e0ba 100644 --- a/tests/unit/request-queue.js +++ b/tests/unit/request-queue.js @@ -21,7 +21,7 @@ describe(`RequestQueue`, function() { } clock = sinon.useFakeTimers(START_TIME); localStorage.clear(); - queue = new RequestQueue(`fake-rq-key`, {storage: localStorage}); + queue = new RequestQueue(`fake-rq-key`, {storage: localStorage, usePersistence: true}); }); afterEach(function() { @@ -76,7 +76,7 @@ describe(`RequestQueue`, function() { }); it(`waits for current lock holder to release`, function(done) { - const firstHolder = new RequestQueue(`fake-rq-key`, {storage: localStorage, pid: `first-holder`}); + const firstHolder = new RequestQueue(`fake-rq-key`, {storage: localStorage, pid: `first-holder`, usePersistence: true}); const firstItem = {event: `first`}; acquireLockForPid(queue.lock, `first-holder`); @@ -136,7 +136,7 @@ describe(`RequestQueue`, function() { context(`mid-acquisition`, function() { beforeEach(function() { localStorage.clear(); - queue = new RequestQueue(`fake-rq-key`, {storage: localStorage, pid: `mypid`}); + queue = new RequestQueue(`fake-rq-key`, {storage: localStorage, pid: `mypid`, usePersistence: true}); sinon.stub(localStorage, `setItem`) .withArgs(`fake-rq-key:Y`, `mypid`) .onCall(0).returns(null) From 15ad6db96fb108a867f89c189e236d6cfa102fb7 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 22 May 2024 14:18:13 +0000 Subject: [PATCH 07/48] fix and add integration tests --- src/recorder/index.js | 2 +- tests/test.js | 105 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 470802bd..fdfe3dea 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -254,7 +254,7 @@ MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (da 'body_data': bodyData, headers: headers, callback: callback, - reportError: _.bind(this.reportError, this) + 'report_error': _.bind(this.reportError, this) }); make_xhr_request(reqOptions); diff --git a/tests/test.js b/tests/test.js index 58394e15..38076abe 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5236,8 +5236,14 @@ this.clock = sinon.useFakeTimers(); this.randomStub = sinon.stub(Math, 'random'); - this.fetchStub = sinon.stub(window, 'fetch'); + startRecordingXhrRequests.call(this) + this.getRecordRequests = _.bind(function () { + return this.requests.filter(function (req) { + return req.url === 'https://api-js.mixpanel.com/record/'; + }); + }, this) + var recorderSrc = window.MIXPANEL_CUSTOM_LIB_URL === '../build/mixpanel.js' ? '../build/mixpanel-recorder.js' : '../build/mixpanel-recorder.min.js'; @@ -5270,10 +5276,10 @@ mixpanel.recordertest.stop_session_recording(); clearLibInstance(mixpanel.recordertest); } - + stopRecordingXhrRequests.call(this); + this.clock.restore(); this.randomStub.restore(); - this.fetchStub.restore(); var scriptEl = this.getRecorderScript(); if (scriptEl) { @@ -5281,7 +5287,11 @@ } delete window['__mp_recorder']; } - }) + }); + + function respondSuccess(request) { + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({code: 200, status: "OK"})); + } asyncTest('adds script tag when sampled', 2, function () { this.randomStub.returns(0.02); @@ -5362,7 +5372,7 @@ ok(this.getRecorderScript() === null); this.clock.tick(10 * 1000); - same(this.fetchStub.getCalls().length, 0, 'no /record call has been made since the user did not fall into the sample.'); + same(this.getRecordRequests().length, 0, 'no /record call has been made since the user did not fall into the sample.'); mixpanel.recordertest.start_session_recording(); @@ -5469,6 +5479,91 @@ }, this), 2); } }, this)); + same(this.getRecordRequests().length, 1, 'no /record calls made after user has opted out.'); + mixpanel.recordertest.stop_session_recording(); + }); + }); + + asyncTest('retries record request after a 500', 12, function () { + this.randomStub.returns(0.02); + this.initMixpanelRecorder({record_sessions_percent: 10}); + ok(this.getRecorderScript() !== null); + + this.afterRecorderLoaded.call(this, function () { + simulateMouseClick(document.body); + this.clock.tick(10 * 1000) + same(this.getRecordRequests().length, 1, 'one batch fetch request made every ten seconds'); + + var request = this.getRecordRequests()[0] + same(request.url, "https://api-js.mixpanel.com/record/"); + var payload = JSON.parse(request.requestBody); + same(payload.distinct_id, mixpanel.recordertest.get_distinct_id()); + same(payload.$device_id, mixpanel.recordertest.get_property('$device_id')); + ok(payload.events.length > 0); + respondSuccess(request); + + simulateMouseClick(document.body); + this.clock.tick(10 * 1000); + same(this.getRecordRequests().length, 2, 'one batch fetch request made every ten seconds'); + + this.getRecordRequests()[1].respond(500, {'Content-Type': 'text'}, "error"); + this.clock.tick(20 * 2000); + same(this.getRecordRequests().length, 3, 'record request is retried after a 500'); + request = this.getRecordRequests()[2]; + payload = JSON.parse(request.requestBody); + ok(request.url === "https://api-js.mixpanel.com/record/") + ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); + ok(payload.events.length > 0); + mixpanel.recordertest.stop_session_recording(); + }); + }); + + asyncTest('halves batch size and retries record request after a 413', 17, function () { + this.randomStub.returns(0.02); + this.initMixpanelRecorder({record_sessions_percent: 10}); + ok(this.getRecorderScript() !== null); + + this.afterRecorderLoaded.call(this, function () { + simulateMouseClick(document.body); + this.clock.tick(10 * 1000) + same(this.getRecordRequests().length, 1, 'one batch fetch request made every ten seconds'); + + var request = this.getRecordRequests()[0] + same(request.url, "https://api-js.mixpanel.com/record/"); + var payload = JSON.parse(request.requestBody); + same(payload.distinct_id, mixpanel.recordertest.get_distinct_id()); + same(payload.$device_id, mixpanel.recordertest.get_property('$device_id')); + ok(payload.events.length > 0); + respondSuccess(request); + + this.randomStub.restore(); + for (var _i = 0; _i < 1000; _i++) { + simulateMouseClick(document.body); + } + this.clock.tick(10 * 1000); + same(this.getRecordRequests().length, 2, 'one batch fetch request made every ten seconds'); + request = this.getRecordRequests()[1] + payload = JSON.parse(request.requestBody); + same(payload.events.length, 1000); + request.respond(413, {'Content-Type': 'text'}, "error"); + + this.clock.tick(10 * 1000); + same(this.getRecordRequests().length, 3, 'record request is retried after a 413'); + request = this.getRecordRequests()[2]; + payload = JSON.parse(request.requestBody); + ok(request.url === "https://api-js.mixpanel.com/record/") + ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); + same(payload.events.length, 500, 'batch request was halved'); + respondSuccess(request); + this.clock.tick(10 * 1000); + same(this.getRecordRequests().length, 4, 'remaining requests in the queue are flushed'); + request = this.getRecordRequests()[3]; + payload = JSON.parse(request.requestBody); + ok(request.url === "https://api-js.mixpanel.com/record/") + ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); + same(payload.events.length, 500, 'batch request was halved'); + + mixpanel.recordertest.stop_session_recording(); }); }); } From ba60d404bfeb26be9a0c86afe650819d54ffd5f9 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 22 May 2024 14:19:11 +0000 Subject: [PATCH 08/48] sendBeacon garbage --- src/recorder/index.js | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index fdfe3dea..512e13f3 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -44,23 +44,6 @@ MixpanelRecorder.prototype._initBatcher = function () { }, this), forceDelayFlush: true, }); - - // var flushOnUnload = _.bind(function() { - // if (!this.batcher.stopped) { - // this.batcher.flush({unloading: true}); - // } - // }, this); - - // window.addEventListener('pagehide', function(ev) { - // if (ev['persisted']) { - // flushOnUnload(); - // } - // }); - // window.addEventListener('visibilitychange', function() { - // if (document['visibilityState'] === 'hidden') { - // flushOnUnload(); - // } - // }); }; // eslint-disable-next-line camelcase @@ -235,19 +218,6 @@ MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (da } var bodyData = _.JSONEncode(reqBody); - - // if (options.transport === 'sendBeacon') { - // // we have access to fetch in this environment due to MutationObserver requirement. - // // use it as a replacement for sendBeacon so that we can set header authorization. - // window['fetch'](url, { - // method: 'POST', - // headers: headers, - // body: bodyData, - // keepalive: true, - // }); - // callback(true); - // } - var reqOptions = _.extend({}, options, { method: 'POST', url: url, From c62f9093996dac5e133628e951f8fff8ccad413d Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Thu, 23 May 2024 20:39:43 +0000 Subject: [PATCH 09/48] normalize batcher callback --- src/request-batcher.js | 7 +++---- src/utils.js | 14 ++++++++++---- tests/unit/request-batcher.js | 6 ++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/request-batcher.js b/src/request-batcher.js index 541bd9c8..7fec64eb 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -186,12 +186,11 @@ RequestBatcher.prototype.flush = function(options) { this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.status >= 500 || res.status === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.currentFlushInterval * 2; - var headers = res.xhr_req['responseHeaders']; + var headers = res.responseHeaders; if (headers) { var retryAfter = headers['Retry-After']; if (retryAfter) { @@ -201,7 +200,7 @@ RequestBatcher.prototype.flush = function(options) { retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.status === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(flushBatchSize / 2)); diff --git a/src/utils.js b/src/utils.js index 8f951d3d..facf64e7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1686,6 +1686,12 @@ var cheap_guid = function(maxlen) { * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. * @param {Function} [options.callback] - The callback function to execute when the request completes. + * callback: response { + * status?: number, + * error?: string, + * responseBody?: Object|string + * responseHeader?: Object + * } | null; * @param {Function} [options.report_error] - The function to execute when an error occurs. * @param {string|Object} [options.body_data] - The data to send with the request, if any. */ @@ -1710,13 +1716,13 @@ var make_xhr_request = function (options) { if (req.status === 200) { if (options.callback) { if (options.verbose) { - var response; + var response = {}; try { - response = _.JSONDecode(req.responseText); + response['responseBody'] = _.JSONDecode(req.responseText); } catch (e) { options.report_error(e); if (options.ignore_json_errors) { - response = req.responseText; + response['responseBody'] = req.responseText; } else { return; } @@ -1740,7 +1746,7 @@ var make_xhr_request = function (options) { options.report_error(error); if (options.callback) { if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); + options.callback({status: req.status, error: error, responseHeaders: req.responseHeaders}); } else { options.callback(0); } diff --git a/tests/unit/request-batcher.js b/tests/unit/request-batcher.js index 3d9b725d..fc5e7955 100644 --- a/tests/unit/request-batcher.js +++ b/tests/unit/request-batcher.js @@ -31,10 +31,8 @@ describe(`RequestBatcher`, function() { // respond to last request sent const requestIndex = batcher.options.sendRequestFunc.args.length - 1; batcher.options.sendRequestFunc.args[requestIndex][2]({ - 'xhr_req': { - status, - responseHeaders, - }, + status, + responseHeaders, error, }); } From ef68b1de8ca066f0f6f7663b7cc32c6c4b01e00a Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 24 May 2024 15:02:14 +0000 Subject: [PATCH 10/48] fix integration tests --- src/mixpanel-core.js | 71 +++++++++++++++++++----- src/recorder/index.js | 31 ++++++----- src/request-batcher.js | 28 +++++++--- src/utils.js | 84 ----------------------------- tests/test.js | 120 +++++++++++++++++++++++++---------------- 5 files changed, 171 insertions(+), 163 deletions(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index d0e6d803..5451385c 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -1,6 +1,6 @@ /* eslint camelcase: "off" */ import Config from './config'; -import { MAX_RECORDING_MS, _, console, userAgent, window, document, navigator, slice, make_xhr_request} from './utils'; +import { MAX_RECORDING_MS, _, console, userAgent, window, document, navigator, slice } from './utils'; import { FormTracker, LinkTracker } from './dom-trackers'; import { RequestBatcher } from './request-batcher'; import { MixpanelGroup } from './mixpanel-group'; @@ -626,22 +626,69 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response = {}; + try { + response['responseBody'] = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response['responseBody'] = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: req.status, error: error, retryAfter: req.responseHeaders['Retry-After']}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; diff --git a/src/recorder/index.js b/src/recorder/index.js index 512e13f3..0304551c 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -191,12 +191,6 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() { }); MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (data, options, callback) { - var url = this.get_config('api_host') + '/' + this.get_config('api_routes')['record']; - var headers = { - 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), - 'Content-Type': 'application/json' - }; - var reqBody = { 'distinct_id': String(this._mixpanel.get_distinct_id()), 'events': data, @@ -217,17 +211,22 @@ MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (da reqBody['$user_id'] = userId; } - var bodyData = _.JSONEncode(reqBody); - var reqOptions = _.extend({}, options, { - method: 'POST', - url: url, - 'body_data': bodyData, - headers: headers, - callback: callback, - 'report_error': _.bind(this.reportError, this) + window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'], { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/json' + }, + 'body': _.JSONEncode(reqBody) + }).then(function (response) { + response.json().then(function (responseBody) { + callback({status: response.status, responseBody: responseBody, retryAfter: response.headers.get('Retry-After')}); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); - - make_xhr_request(reqOptions); }); diff --git a/src/request-batcher.js b/src/request-batcher.js index 7fec64eb..80f0dc4c 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -12,6 +12,26 @@ var logger = console_with_prefix('batch'); * type (events, people, groups). * Uses RequestQueue to manage the backing store. * @constructor + * @param {string} storageKey - Key to access the storage for request queue. + * @param {Object} options - Configuration options for the RequestBatcher. + * @param {number} options.batchSize - The size of the batch to be sent in each flush. + * @param {number} options.flushIntervalMs - Interval in milliseconds between each flush attempt. + * @param {boolean} options.usePersistence - Whether to use persistent storage. + * @param {boolean} options.forceDelayFlush - Force flush at the interval specified by flushIntervalMs. + * @param {boolean} options.autoStart - Automatically start the batcher upon initialization. + * @param {Object} options.storage - Storage implementation to use. + * @param {function} options.errorReporter - Function to report errors. + * @param {function} options.sendRequestFunc - Function to send the request. Takes three arguments: + * - data: Array of payload objects to be sent. + * - requestOptions: Object containing request options (method, timeout, transport type, etc.). + * - callback: Function to be called with the response. Should be called with an object containing optional fields: + * - {number} [status] - HTTP status code of the response. + * - {string} [error] - Error message if the request failed. + * - {string} [retryAfter] - Value of the 'Retry-After' header + * - {Object|string} [responseBody] - Body of the response. + * @param {function} options.stopAllBatchingFunc - Function to stop all batching operations. + * @param {function} [options.beforeSendHook] - Hook to modify payload before sending. + * @param {number} [options.requestTimeoutMs] - Timeout for each request. */ var RequestBatcher = function(storageKey, options) { this.options = options; @@ -190,12 +210,8 @@ RequestBatcher.prototype.flush = function(options) { ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.currentFlushInterval * 2; - var headers = res.responseHeaders; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); diff --git a/src/utils.js b/src/utils.js index facf64e7..877e29d0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1674,89 +1674,6 @@ var cheap_guid = function(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; - -/** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * callback: response { - * status?: number, - * error?: string, - * responseBody?: Object|string - * responseHeader?: Object - * } | null; - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ -var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response = {}; - try { - response['responseBody'] = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response['responseBody'] = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: req.status, error: error, responseHeaders: req.responseHeaders}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); -}; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -1821,5 +1738,4 @@ export { JSONStringify, JSONParse, slice, - make_xhr_request }; diff --git a/tests/test.js b/tests/test.js index 38076abe..3193aa8e 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5230,20 +5230,46 @@ if (window.MutationObserver) { + // fake synchronous promise, used to avoid nesting in tests + // while testing callback logic + function fakePromiseWrap(val) { + return { + then: function(cb) { + cb(val); + return fakePromiseWrap(val); + }, + catch: function() {}, + } + } + + function makeFakeFetchResponse(status, body, cb) { + body = body || {} + var response = new Response({}, { + status: status, + headers: { + 'Content-type': 'application/json' + } + }); + + return fakePromiseWrap({ + json: function() { + return fakePromiseWrap(JSON.stringify(body)) + }, + status: response.status, + headers: response.headers, + } + ); + } + module('recorder', { setup: function () { this.token = rand_name(); this.clock = sinon.useFakeTimers(); this.randomStub = sinon.stub(Math, 'random'); - startRecordingXhrRequests.call(this) + this.fetchStub = sinon.stub(window, 'fetch'); + this.fetchStub.returns(makeFakeFetchResponse(200)); - this.getRecordRequests = _.bind(function () { - return this.requests.filter(function (req) { - return req.url === 'https://api-js.mixpanel.com/record/'; - }); - }, this) - var recorderSrc = window.MIXPANEL_CUSTOM_LIB_URL === '../build/mixpanel.js' ? '../build/mixpanel-recorder.js' : '../build/mixpanel-recorder.min.js'; @@ -5276,10 +5302,10 @@ mixpanel.recordertest.stop_session_recording(); clearLibInstance(mixpanel.recordertest); } - stopRecordingXhrRequests.call(this); - + this.clock.restore(); this.randomStub.restore(); + this.fetchStub.restore(); var scriptEl = this.getRecorderScript(); if (scriptEl) { @@ -5287,11 +5313,7 @@ } delete window['__mp_recorder']; } - }); - - function respondSuccess(request) { - request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({code: 200, status: "OK"})); - } + }) asyncTest('adds script tag when sampled', 2, function () { this.randomStub.returns(0.02); @@ -5372,7 +5394,7 @@ ok(this.getRecorderScript() === null); this.clock.tick(10 * 1000); - same(this.getRecordRequests().length, 0, 'no /record call has been made since the user did not fall into the sample.'); + same(this.fetchStub.getCalls().length, 0, 'no /record call has been made since the user did not fall into the sample.'); mixpanel.recordertest.start_session_recording(); @@ -5488,30 +5510,32 @@ this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null); + this.fetchStub.onFirstCall() + .returns(makeFakeFetchResponse(200)) + .onSecondCall() + .returns(makeFakeFetchResponse(500)); this.afterRecorderLoaded.call(this, function () { simulateMouseClick(document.body); this.clock.tick(10 * 1000) - same(this.getRecordRequests().length, 1, 'one batch fetch request made every ten seconds'); + same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); - var request = this.getRecordRequests()[0] - same(request.url, "https://api-js.mixpanel.com/record/"); - var payload = JSON.parse(request.requestBody); + var callArgs = this.fetchStub.getCall(0).args; + same(callArgs[0], "https://api-js.mixpanel.com/record/"); + var payload = JSON.parse(callArgs[1].body); same(payload.distinct_id, mixpanel.recordertest.get_distinct_id()); same(payload.$device_id, mixpanel.recordertest.get_property('$device_id')); ok(payload.events.length > 0); - respondSuccess(request); simulateMouseClick(document.body); this.clock.tick(10 * 1000); - same(this.getRecordRequests().length, 2, 'one batch fetch request made every ten seconds'); + same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); - this.getRecordRequests()[1].respond(500, {'Content-Type': 'text'}, "error"); this.clock.tick(20 * 2000); - same(this.getRecordRequests().length, 3, 'record request is retried after a 500'); - request = this.getRecordRequests()[2]; - payload = JSON.parse(request.requestBody); - ok(request.url === "https://api-js.mixpanel.com/record/") + same(this.fetchStub.getCalls().length, 3, 'record request is retried after a 500'); + callArgs = this.fetchStub.getCalls()[2].args; + payload = JSON.parse(callArgs[1].body); + ok(callArgs[0] === "https://api-js.mixpanel.com/record/") ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); ok(payload.events.length > 0); mixpanel.recordertest.stop_session_recording(); @@ -5522,44 +5546,50 @@ this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null); + this.fetchStub.onCall(0) + .returns(makeFakeFetchResponse(200)) + .onCall(1) + .returns(makeFakeFetchResponse(413)) + .onCall(2) + .returns(makeFakeFetchResponse(200)) + .onCall(3) + .returns(makeFakeFetchResponse(200)) this.afterRecorderLoaded.call(this, function () { simulateMouseClick(document.body); - this.clock.tick(10 * 1000) - same(this.getRecordRequests().length, 1, 'one batch fetch request made every ten seconds'); + this.clock.tick(10 * 1000); + same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); - var request = this.getRecordRequests()[0] - same(request.url, "https://api-js.mixpanel.com/record/"); - var payload = JSON.parse(request.requestBody); + var callArgs = this.fetchStub.getCall(0).args; + same(callArgs[0], "https://api-js.mixpanel.com/record/"); + var payload = JSON.parse(callArgs[1].body); same(payload.distinct_id, mixpanel.recordertest.get_distinct_id()); same(payload.$device_id, mixpanel.recordertest.get_property('$device_id')); ok(payload.events.length > 0); - respondSuccess(request); this.randomStub.restore(); for (var _i = 0; _i < 1000; _i++) { simulateMouseClick(document.body); } this.clock.tick(10 * 1000); - same(this.getRecordRequests().length, 2, 'one batch fetch request made every ten seconds'); - request = this.getRecordRequests()[1] - payload = JSON.parse(request.requestBody); + same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); + callArgs = this.fetchStub.getCall(1).args; + payload = JSON.parse(callArgs[1].body); same(payload.events.length, 1000); - request.respond(413, {'Content-Type': 'text'}, "error"); this.clock.tick(10 * 1000); - same(this.getRecordRequests().length, 3, 'record request is retried after a 413'); - request = this.getRecordRequests()[2]; - payload = JSON.parse(request.requestBody); - ok(request.url === "https://api-js.mixpanel.com/record/") + same(this.fetchStub.getCalls().length, 3, 'record request is retried after a 413'); + callArgs = this.fetchStub.getCall(2).args; + payload = JSON.parse(callArgs[1].body); + ok(callArgs[0] === "https://api-js.mixpanel.com/record/") ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); same(payload.events.length, 500, 'batch request was halved'); - respondSuccess(request); + this.clock.tick(10 * 1000); - same(this.getRecordRequests().length, 4, 'remaining requests in the queue are flushed'); - request = this.getRecordRequests()[3]; - payload = JSON.parse(request.requestBody); - ok(request.url === "https://api-js.mixpanel.com/record/") + same(this.fetchStub.getCalls().length, 4, 'remaining requests in the queue are flushed'); + callArgs = this.fetchStub.getCall(3).args; + payload = JSON.parse(callArgs[1].body); + ok(callArgs[0] === "https://api-js.mixpanel.com/record/") ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); same(payload.events.length, 500, 'batch request was halved'); From 39f1b6f67192495bb8f651b4287293f7eec93c03 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 24 May 2024 15:11:38 +0000 Subject: [PATCH 11/48] fix tests --- src/recorder/index.js | 2 +- tests/unit/request-batcher.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 0304551c..cdb1fdfb 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -1,6 +1,6 @@ import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js'; -import { MAX_RECORDING_MS, console_with_prefix, _, make_xhr_request } from '../utils'; // eslint-disable-line camelcase +import { MAX_RECORDING_MS, console_with_prefix, _, } from '../utils'; // eslint-disable-line camelcase import { addOptOutCheckMixpanelLib } from '../gdpr-utils'; import { RequestBatcher } from '../request-batcher'; diff --git a/tests/unit/request-batcher.js b/tests/unit/request-batcher.js index fc5e7955..8c9dcf8a 100644 --- a/tests/unit/request-batcher.js +++ b/tests/unit/request-batcher.js @@ -27,12 +27,12 @@ describe(`RequestBatcher`, function() { return JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY)); } - function sendResponse(status, {error, responseHeaders} = {}) { + function sendResponse(status, {error, retryAfter} = {}) { // respond to last request sent const requestIndex = batcher.options.sendRequestFunc.args.length - 1; batcher.options.sendRequestFunc.args[requestIndex][2]({ status, - responseHeaders, + retryAfter, error, }); } @@ -439,7 +439,7 @@ describe(`RequestBatcher`, function() { batcher.enqueue({ev: `queued event 2`}); batcher.flush(); - sendResponse(503, {responseHeaders: {'Retry-After': `20`}}); + sendResponse(503, {retryAfter: `20`}); clock.tick(10000); expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no retry yet clock.tick(10000); From ce7daa7977a05cac8fa872114d0f9ffc73ee14ee Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 25 Jun 2024 13:43:25 +0000 Subject: [PATCH 12/48] undo compiled --- dist/mixpanel-recorder.js | 973 +------------------------ dist/mixpanel-recorder.min.js | 23 - dist/mixpanel.amd.js | 246 +++---- dist/mixpanel.cjs.js | 246 +++---- dist/mixpanel.globals.js | 246 +++---- dist/mixpanel.min.js | 113 --- dist/mixpanel.umd.js | 246 +++---- examples/commonjs-browserify/bundle.js | 246 +++---- examples/es2015-babelify/bundle.js | 236 +++--- examples/umd-webpack/bundle.js | 246 +++---- src/mixpanel-core.js | 2 +- 11 files changed, 625 insertions(+), 2198 deletions(-) diff --git a/dist/mixpanel-recorder.js b/dist/mixpanel-recorder.js index eca2171c..f8f434fd 100644 --- a/dist/mixpanel-recorder.js +++ b/dist/mixpanel-recorder.js @@ -4478,13 +4478,8 @@ record.mirror = mirror; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -4547,41 +4542,12 @@ var console$1 = { /** @type {function(...*)} */ log: function() { - if (!_.isUndefined(windowConsole) && windowConsole) { - try { - windowConsole.log.apply(windowConsole, arguments); - } catch (err) { - _.each(arguments, function(arg) { - windowConsole.log(arg); - }); - } - } }, /** @type {function(...*)} */ warn: function() { - if (!_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); - try { - windowConsole.warn.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.warn(arg); - }); - } - } }, /** @type {function(...*)} */ error: function() { - if (!_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } }, /** @type {function(...*)} */ critical: function() { @@ -6162,83 +6128,6 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; - - /** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ - var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); - }; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -6455,760 +6344,9 @@ }; } - var logger$3 = console_with_prefix('lock'); - - /** - * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser - * window/tab at a time will be able to access shared resources. - * - * Based on the Alur and Taubenfeld fast lock - * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) - * with an added timeout to ensure there will be eventual progress in the event - * that a window is closed in the middle of the callback. - * - * Implementation based on the original version by David Wolever (https://github.com/wolever) - * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. - * - * @example - * const myLock = new SharedLock('some-key'); - * myLock.withLock(function() { - * console.log('I hold the mutex!'); - * }); - * - * @constructor - */ - var SharedLock = function(key, options) { - options = options || {}; - - this.storageKey = key; - this.storage = options.storage || window.localStorage; - this.pollIntervalMS = options.pollIntervalMS || 100; - this.timeoutMS = options.timeoutMS || 2000; - }; - - // pass in a specific pid to test contention scenarios; otherwise - // it is chosen randomly for each acquisition attempt - SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { - if (!pid && typeof errorCB !== 'function') { - pid = errorCB; - errorCB = null; - } - - var i = pid || (new Date().getTime() + '|' + Math.random()); - var startTime = new Date().getTime(); - - var key = this.storageKey; - var pollIntervalMS = this.pollIntervalMS; - var timeoutMS = this.timeoutMS; - var storage = this.storage; - - var keyX = key + ':X'; - var keyY = key + ':Y'; - var keyZ = key + ':Z'; - - var reportError = function(err) { - errorCB && errorCB(err); - }; - - var delay = function(cb) { - if (new Date().getTime() - startTime > timeoutMS) { - logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); - storage.removeItem(keyZ); - storage.removeItem(keyY); - loop(); - return; - } - setTimeout(function() { - try { - cb(); - } catch(err) { - reportError(err); - } - }, pollIntervalMS * (Math.random() + 0.1)); - }; - - var waitFor = function(predicate, cb) { - if (predicate()) { - cb(); - } else { - delay(function() { - waitFor(predicate, cb); - }); - } - }; - - var getSetY = function() { - var valY = storage.getItem(keyY); - if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) - return false; - } else { - storage.setItem(keyY, i); - if (storage.getItem(keyY) === i) { - return true; - } else { - if (!localStorageSupported(storage, true)) { - throw new Error('localStorage support dropped while acquiring lock'); - } - return false; - } - } - }; - - var loop = function() { - storage.setItem(keyX, i); - - waitFor(getSetY, function() { - if (storage.getItem(keyX) === i) { - criticalSection(); - return; - } - - delay(function() { - if (storage.getItem(keyY) !== i) { - loop(); - return; - } - waitFor(function() { - return !storage.getItem(keyZ); - }, criticalSection); - }); - }); - }; - - var criticalSection = function() { - storage.setItem(keyZ, '1'); - try { - lockedCB(); - } finally { - storage.removeItem(keyZ); - if (storage.getItem(keyY) === i) { - storage.removeItem(keyY); - } - if (storage.getItem(keyX) === i) { - storage.removeItem(keyX); - } - } - }; - - try { - if (localStorageSupported(storage, true)) { - loop(); - } else { - throw new Error('localStorage support check failed'); - } - } catch(err) { - reportError(err); - } - }; - - var logger$2 = console_with_prefix('batch'); - - /** - * RequestQueue: queue for batching API requests with localStorage backup for retries. - * Maintains an in-memory queue which represents the source of truth for the current - * page, but also writes all items out to a copy in the browser's localStorage, which - * can be read on subsequent pageloads and retried. For batchability, all the request - * items in the queue should be of the same type (events, people updates, group updates) - * so they can be sent in a single request to the same API endpoint. - * - * LocalStorage keying and locking: In order for reloads and subsequent pageloads of - * the same site to access the same persisted data, they must share the same localStorage - * key (for instance based on project token and queue type). Therefore access to the - * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent - * simultaneously open windows/tabs from overwriting each other's data (which would lead - * to data loss in some situations). - * @constructor - */ - var RequestQueue = function(storageKey, options) { - options = options || {}; - this.storageKey = storageKey; - this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); - this.lock = new SharedLock(storageKey, {storage: this.storage}); - - this.usePersistence = options.usePersistence; - this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios - - this.memQueue = []; - }; - - /** - * Add one item to queues (memory and localStorage). The queued entry includes - * the given item along with an auto-generated ID and a "flush-after" timestamp. - * It is expected that the item will be sent over the network and dequeued - * before the flush-after time; if this doesn't happen it is considered orphaned - * (e.g., the original tab where it was enqueued got closed before it could be - * sent) and the item can be sent by any tab that finds it in localStorage. - * - * The final callback param is called with a param indicating success or - * failure of the enqueue operation; it is asynchronous because the localStorage - * lock is asynchronous. - */ - RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { - var queueEntry = { - 'id': cheap_guid(), - 'flushAfter': new Date().getTime() + flushInterval * 2, - 'payload': item - }; - - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); - }; - - - /** - * Read out the given number of queue entries. If this.memQueue - * has fewer than batchSize items, then look for "orphaned" items - * in the persisted queue (items where the 'flushAfter' time has - * already passed). - */ - RequestQueue.prototype.fillBatch = function(batchSize) { - var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { - // don't need lock just to read events; localStorage is thread-safe - // and the worst that could happen is a duplicate send of some - // orphaned events, which will be deduplicated on the server side - var storedQueue = this.readFromStorage(); - if (storedQueue.length) { - // item IDs already in batch; don't duplicate out of storage - var idsInBatch = {}; // poor man's Set - _.each(batch, function(item) { idsInBatch[item['id']] = true; }); - - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { - item.orphaned = true; - batch.push(item); - if (batch.length >= batchSize) { - break; - } - } - } - } - } - return batch; - }; - - /** - * Remove items with matching 'id' from array (immutably) - * also remove any item without a valid id (e.g., malformed - * storage entries). - */ - var filterOutIDsAndInvalid = function(items, idSet) { - var filteredItems = []; - _.each(items, function(item) { - if (item['id'] && !idSet[item['id']]) { - filteredItems.push(item); - } - }); - return filteredItems; - }; - - /** - * Remove items with matching IDs from both in-memory queue - * and persisted queue - */ - RequestQueue.prototype.removeItemsByID = function(ids, cb) { - var idSet = {}; // poor man's Set - _.each(ids, function(id) { idSet[id] = true; }); - - this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; - } - } - } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); - - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); - } - } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); - }; - - // internal helper for RequestQueue.updatePayloads - var updatePayloads = function(existingItems, itemsToUpdate) { - var newItems = []; - _.each(existingItems, function(item) { - var id = item['id']; - if (id in itemsToUpdate) { - var newPayload = itemsToUpdate[id]; - if (newPayload !== null) { - item['payload'] = newPayload; - newItems.push(item); - } - } else { - // no update - newItems.push(item); - } - }); - return newItems; - }; - - /** - * Update payloads of given items in both in-memory queue and - * persisted queue. Items set to null are removed from queues. - */ - RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { - this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); - }; - - /** - * Read and parse items array from localStorage entry, handling - * malformed/missing data if necessary. - */ - RequestQueue.prototype.readFromStorage = function() { - var storageEntry; - try { - storageEntry = this.storage.getItem(this.storageKey); - if (storageEntry) { - storageEntry = JSONParse(storageEntry); - if (!_.isArray(storageEntry)) { - this.reportError('Invalid storage entry:', storageEntry); - storageEntry = null; - } - } - } catch (err) { - this.reportError('Error retrieving queue', err); - storageEntry = null; - } - return storageEntry || []; - }; - - /** - * Serialize the given items array to localStorage. - */ - RequestQueue.prototype.saveToStorage = function(queue) { - try { - this.storage.setItem(this.storageKey, JSONStringify(queue)); - return true; - } catch (err) { - this.reportError('Error saving queue', err); - return false; - } - }; - - /** - * Clear out queues (memory and localStorage). - */ - RequestQueue.prototype.clear = function() { - this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } - }; - - // maximum interval between request retries after exponential backoff - var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - - var logger$1 = console_with_prefix('batch'); - - /** - * RequestBatcher: manages the queueing, flushing, retry etc of requests of one - * type (events, people, groups). - * Uses RequestQueue to manage the backing store. - * @constructor - */ - var RequestBatcher = function(storageKey, options) { - this.options = options; - - this.queue = new RequestQueue(storageKey, { - errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence - }); - - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; - - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; - - this.stopped = !this.options.autoStart; - this.consecutiveRemovalFailures = 0; - - // extra client-side dedupe - this.itemIdsSentSuccessfully = {}; - }; - - /** - * Add one item to queue. - */ - RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); - }; - - /** - * Start flushing batches at the configured time interval. Must call - * this method upon SDK init in order to send anything over the network. - */ - RequestBatcher.prototype.start = function() { - this.stopped = false; - this.consecutiveRemovalFailures = 0; - this.flush(); - }; - - /** - * Stop flushing batches. Can be restarted by calling start(). - */ - RequestBatcher.prototype.stop = function() { - this.stopped = true; - if (this.timeoutID) { - clearTimeout(this.timeoutID); - this.timeoutID = null; - } - }; - - /** - * Clear out queue. - */ - RequestBatcher.prototype.clear = function() { - this.queue.clear(); - }; - - /** - * Restore batch size configuration to the originally initialized value - */ - RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; - }; - - /** - * Restore flush interval time configuration to the originally initialized value - */ - RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); - }; - - /** - * Schedule the next flush in the given number of milliseconds. - */ - RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; - if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); - } - }; - - /** - * Flush one batch to network. Depending on success/failure modes, it will either - * remove the batch from the queue or leave it in for retry, and schedule the next - * flush. In cases of most network or API failures, it will back off exponentially - * when retrying. - * @param {Object} [options] - * @param {boolean} [options.sendBeacon] - whether to send batch with - * navigator.sendBeacon (only useful for sending batches before page unloads, as - * sendBeacon offers no callbacks or status indications) - */ - RequestBatcher.prototype.flush = function(options) { - try { - - if (this.requestInProgress) { - logger$1.log('Flush: Request already in progress'); - return; - } - - options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; - var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; - var batch = this.queue.fillBatch(currentBatchSize); - var dataForRequest = []; - var transformedItems = {}; - _.each(batch, function(item) { - var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); - } - if (payload) { - // mp_sent_by_lib_version prop captures which lib version actually - // sends each event (regardless of which version originally queued - // it for sending) - if (payload['event'] && payload['properties']) { - payload['properties'] = _.extend( - {}, - payload['properties'], - {'mp_sent_by_lib_version': Config.LIB_VERSION} - ); - } - var addPayload = true; - var itemId = item['id']; - if (itemId) { - if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { - this.reportError('[dupe] item ID sent too many times, not sending', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - addPayload = false; - } - } else { - this.reportError('[dupe] found item with no ID', {item: item}); - } - - if (addPayload) { - dataForRequest.push(payload); - } - } - transformedItems[item['id']] = payload; - }, this); - if (dataForRequest.length < 1) { - this.resetFlush(); - return; // nothing to do - } - - this.requestInProgress = true; - - var batchSendCallback = _.bind(function(res) { - this.requestInProgress = false; - - try { - - // handle API response in a try-catch to make sure we can reset the - // flush operation if something goes wrong - - var removeItemsFromQueue = false; - if (options.unloading) { - // update persisted data to include hook transformations - this.queue.updatePayloads(transformedItems); - } else if ( - _.isObject(res) && - res.error === 'timeout' && - new Date().getTime() - startTime >= timeoutMS - ) { - this.reportError('Network timeout; retrying'); - this.flush(); - } else if ( - _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') - ) { - // network or API error, or 429 Too Many Requests, retry - var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } - } - retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); - this.reportError('Error; retry in ' + retryMS + ' ms'); - this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { - // 413 Payload Too Large - if (batch.length > 1) { - var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); - this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); - this.reportError('413 response; reducing batch size to ' + this.batchSize); - this.resetFlush(); - } else { - this.reportError('Single-event request too large; dropping', batch); - this.resetBatchSize(); - removeItemsFromQueue = true; - } - } else { - // successful network request+response; remove each item in batch from queue - // (even if it was e.g. a 400, in which case retrying won't help) - removeItemsFromQueue = true; - } - - if (removeItemsFromQueue) { - this.queue.removeItemsByID( - _.map(batch, function(item) { return item['id']; }), - _.bind(function(succeeded) { - if (succeeded) { - this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } - } else { - this.reportError('Failed to remove items from queue'); - if (++this.consecutiveRemovalFailures > 5) { - this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); - } else { - this.resetFlush(); - } - } - }, this) - ); - - // client-side dedupe - _.each(batch, _.bind(function(item) { - var itemId = item['id']; - if (itemId) { - this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; - this.itemIdsSentSuccessfully[itemId]++; - if (this.itemIdsSentSuccessfully[itemId] > 5) { - this.reportError('[dupe] item ID sent too many times', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - } - } else { - this.reportError('[dupe] found item with no ID while removing', {item: item}); - } - }, this)); - } - - } catch(err) { - this.reportError('Error handling API response', err); - this.resetFlush(); - } - }, this); - var requestOptions = { - method: 'POST', - verbose: true, - ignore_json_errors: true, // eslint-disable-line camelcase - timeout_ms: timeoutMS // eslint-disable-line camelcase - }; - if (options.unloading) { - requestOptions.transport = 'sendBeacon'; - } - logger$1.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { - this.reportError('Error flushing request queue', err); - this.resetFlush(); - } - }; - - /** - * Log error to global logger and optional user-defined logger. - */ - RequestBatcher.prototype.reportError = function(msg, err) { - logger$1.error.apply(logger$1.error, arguments); - if (this.options.errorReporter) { - try { - if (!(err instanceof Error)) { - err = new Error(msg); - } - this.options.errorReporter(msg, err); - } catch(err) { - logger$1.error(err); - } - } - }; - var logger = console_with_prefix('recorder'); var CompressionStream = window['CompressionStream']; - var BATCH_SIZE = 1000; - var BATCH_FLUSH_INTERVAL_MS = 10 * 1000; - var BATCH_REQUEST_TIMEOUT_MS = 90 * 1000; - var MixpanelRecorder = function(mixpanelInstance) { this._mixpanel = mixpanelInstance; @@ -7227,38 +6365,6 @@ this.maxTimeoutId = null; this.recordMaxMs = MAX_RECORDING_MS; - this._initBatcher(); - }; - - - MixpanelRecorder.prototype._initBatcher = function () { - this.batcher = new RequestBatcher('__mprec', { - batchSize: BATCH_SIZE, - flushIntervalMs: BATCH_FLUSH_INTERVAL_MS, - requestTimeoutMs: BATCH_REQUEST_TIMEOUT_MS, - autoStart: true, - sendRequestFunc: _.bind(function(data, options, callback) { - this.sendRequestWithOptOut(data, options, callback); - }, this), - forceDelayFlush: true, - }); - - // var flushOnUnload = _.bind(function() { - // if (!this.batcher.stopped) { - // this.batcher.flush({unloading: true}); - // } - // }, this); - - // window.addEventListener('pagehide', function(ev) { - // if (ev['persisted']) { - // flushOnUnload(); - // } - // }); - // window.addEventListener('visibilitychange', function() { - // if (document['visibilityState'] === 'hidden') { - // flushOnUnload(); - // } - // }); }; // eslint-disable-next-line camelcase @@ -7287,8 +6393,6 @@ this.replayId = _.UUID(); this.replayLengthMs = 0; - this.batcher.start(); - var resetIdleTimeout = _.bind(function () { clearTimeout(this.idleTimeoutId); this.idleTimeoutId = setTimeout(_.bind(function () { @@ -7299,7 +6403,7 @@ this._stopRecording = record({ 'emit': _.bind(function (ev) { - this.batcher.enqueue(ev); + this.recEvents.push(ev); this.replayLengthMs = new Date().getTime() - this.replayStartTime; resetIdleTimeout(); }, this), @@ -7312,6 +6416,7 @@ resetIdleTimeout(); + this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000); this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); }; @@ -7326,9 +6431,10 @@ this._stopRecording = null; } - this.batcher.flush(); // flush any remaining events + this._flushEvents(); // flush any remaining events this.replayId = null; + clearInterval(this.sendBatchId); clearTimeout(this.idleTimeoutId); clearTimeout(this.maxTimeoutId); }; @@ -7337,8 +6443,8 @@ * Flushes the current batch of events to the server, but passes an opt-out callback to make sure * we stop recording and dump any queued events if the user has opted out. */ - MixpanelRecorder.prototype.sendRequestWithOptOut = function (data, options, cb) { - this._sendRequest(data, options, cb, _.bind(this._onOptOut, this)); + MixpanelRecorder.prototype.flushEventsWithOptOut = function () { + this._flushEvents(_.bind(this._onOptOut, this)); }; MixpanelRecorder.prototype._onOptOut = function (code) { @@ -7349,7 +6455,6 @@ } }; -<<<<<<< HEAD MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) { window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { 'method': 'POST', @@ -7403,75 +6508,9 @@ reqParams['format'] = 'body'; this._sendRequest(reqParams, eventsJson); } -======= - MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (data, options, callback) { - var url = this.get_config('api_host') + '/' + this.get_config('api_routes')['record']; - var headers = { - 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), - 'Content-Type': 'application/json' - }; - - var reqBody = { - 'distinct_id': String(this._mixpanel.get_distinct_id()), - 'events': data, - 'seq': this.seqNo++, - 'batch_start_time': this.batchStartTime / 1000, - 'replay_id': this.replayId, - 'replay_length_ms': this.replayLengthMs, - 'replay_start_time': this.replayStartTime / 1000 - }; - - // send ID management props if they exist - var deviceId = this._mixpanel.get_property('$device_id'); - if (deviceId) { - reqBody['$device_id'] = deviceId; ->>>>>>> 571d8b5 (cleanup, draft) - } - var userId = this._mixpanel.get_property('$user_id'); - if (userId) { - reqBody['$user_id'] = userId; } - - var bodyData = _.JSONEncode(reqBody); - - // if (options.transport === 'sendBeacon') { - // // we have access to fetch in this environment due to MutationObserver requirement. - // // use it as a replacement for sendBeacon so that we can set header authorization. - // window['fetch'](url, { - // method: 'POST', - // headers: headers, - // body: bodyData, - // keepalive: true, - // }); - // callback(true); - // } - - var reqOptions = _.extend({}, options, { - method: 'POST', - url: url, - 'body_data': bodyData, - headers: headers, - callback: callback, - reportError: _.bind(this.reportError, this) - }); - - make_xhr_request(reqOptions); }); - - MixpanelRecorder.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); - try { - if (!err && !(msg instanceof Error)) { - msg = new Error(msg); - } - this.get_config('error_reporter')(msg, err); - } catch(err) { - logger.error(err); - } - }; - - window['__mp_recorder'] = MixpanelRecorder; })(); diff --git a/dist/mixpanel-recorder.min.js b/dist/mixpanel-recorder.min.js index 7f1887ab..9cb0d896 100644 --- a/dist/mixpanel-recorder.min.js +++ b/dist/mixpanel-recorder.min.js @@ -1,12 +1,6 @@ -<<<<<<< HEAD (function(){"use strict";var A;(function(e){e[e.Document=0]="Document",e[e.DocumentType=1]="DocumentType",e[e.Element=2]="Element",e[e.Text=3]="Text",e[e.CDATA=4]="CDATA",e[e.Comment=5]="Comment"})(A||(A={}));function ar(e){return e.nodeType===e.ELEMENT_NODE}function ge(e){const t=e?.host;return t?.shadowRoot===e}function ye(e){return Object.prototype.toString.call(e)==="[object ShadowRoot]"}function lr(e){return e.includes(" background-clip: text;")&&!e.includes(" -webkit-background-clip: text;")&&(e=e.replace(" background-clip: text;"," -webkit-background-clip: text; background-clip: text;")),e}function cr(e){const{cssText:t}=e;if(t.split('"').length<3)return t;const r=["@import",`url(${JSON.stringify(e.href)})`];return e.layerName===""?r.push("layer"):e.layerName&&r.push(`layer(${e.layerName})`),e.supportsText&&r.push(`supports(${e.supportsText})`),e.media.length&&r.push(e.media.mediaText),r.join(" ")+";"}function _e(e){try{const t=e.rules||e.cssRules;return t?lr(Array.from(t,ft).join("")):null}catch{return null}}function ft(e){let t;if(dr(e))try{t=_e(e.styleSheet)||cr(e)}catch{}else if(fr(e)&&e.selectorText.includes(":"))return ur(e.cssText);return t||e.cssText}function ur(e){const t=/(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;return e.replace(t,"$1\\$2")}function dr(e){return"styleSheet"in e}function fr(e){return"selectorText"in e}class ht{constructor(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}getId(t){var r;if(!t)return-1;const n=(r=this.getMeta(t))===null||r===void 0?void 0:r.id;return n??-1}getNode(t){return this.idNodeMap.get(t)||null}getIds(){return Array.from(this.idNodeMap.keys())}getMeta(t){return this.nodeMetaMap.get(t)||null}removeNodeFromMap(t){const r=this.getId(t);this.idNodeMap.delete(r),t.childNodes&&t.childNodes.forEach(n=>this.removeNodeFromMap(n))}has(t){return this.idNodeMap.has(t)}hasNode(t){return this.nodeMetaMap.has(t)}add(t,r){const n=r.id;this.idNodeMap.set(n,t),this.nodeMetaMap.set(t,r)}replace(t,r){const n=this.getNode(t);if(n){const i=this.nodeMetaMap.get(n);i&&this.nodeMetaMap.set(r,i)}this.idNodeMap.set(t,r)}reset(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}}function hr(){return new ht}function Ve({element:e,maskInputOptions:t,tagName:r,type:n,value:i,maskInputFn:o}){let a=i||"";const l=n&&te(n);return(t[r.toLowerCase()]||l&&t[l])&&(o?a=o(a,e):a="*".repeat(a.length)),a}function te(e){return e.toLowerCase()}const pt="__rrweb_original__";function pr(e){const t=e.getContext("2d");if(!t)return!0;const r=50;for(let n=0;ns!==0))return!1}return!0}function qe(e){const t=e.type;return e.hasAttribute("data-rr-is-password")?"password":t?te(t):null}function mt(e,t){var r;let n;try{n=new URL(e,t??window.location.href)}catch{return null}const i=/\.([0-9a-z]+)(?:$)/i,o=n.pathname.match(i);return(r=o?.[1])!==null&&r!==void 0?r:null}let mr=1;const gr=new RegExp("[^a-z0-9-_:]"),Se=-2;function gt(){return mr++}function yr(e){if(e instanceof HTMLFormElement)return"form";const t=te(e.tagName);return gr.test(t)?"div":t}function Sr(e){let t="";return e.indexOf("//")>-1?t=e.split("/").slice(0,3).join("/"):t=e.split("/")[0],t=t.split("?")[0],t}let ce,yt;const vr=/url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm,br=/^(?:[a-z+]+:)?\/\//i,wr=/^www\..*/i,Mr=/^(data:)([^,]*),(.*)/i;function xe(e,t){return(e||"").replace(vr,(r,n,i,o,a,l)=>{const s=i||a||l,c=n||o||"";if(!s)return r;if(br.test(s)||wr.test(s))return`url(${c}${s}${c})`;if(Mr.test(s))return`url(${c}${s}${c})`;if(s[0]==="/")return`url(${c}${Sr(t)+s}${c})`;const u=t.split("/"),p=s.split("/");u.pop();for(const m of p)m!=="."&&(m===".."?u.pop():u.push(m));return`url(${c}${u.join("/")}${c})`})}const Ir=/^[^ \t\n\r\u000c]+/,Cr=/^[, \t\n\r\u000c]+/;function Or(e,t){if(t.trim()==="")return t;let r=0;function n(o){let a;const l=o.exec(t.substring(r));return l?(a=l[0],r+=a.length,a):""}const i=[];for(;n(Cr),!(r>=t.length);){let o=n(Ir);if(o.slice(-1)===",")o=ue(e,o.substring(0,o.length-1)),i.push(o);else{let a="";o=ue(e,o);let l=!1;for(;;){const s=t.charAt(r);if(s===""){i.push((o+a).trim());break}else if(l)s===")"&&(l=!1);else if(s===","){r+=1,i.push((o+a).trim());break}else s==="("&&(l=!0);a+=s,r+=1}}}return i.join(", ")}function ue(e,t){if(!t||t.trim()==="")return t;const r=e.createElement("a");return r.href=t,r.href}function _r(e){return!!(e.tagName==="svg"||e.ownerSVGElement)}function Je(){const e=document.createElement("a");return e.href="",e.href}function St(e,t,r,n){return n&&(r==="src"||r==="href"&&!(t==="use"&&n[0]==="#")||r==="xlink:href"&&n[0]!=="#"||r==="background"&&(t==="table"||t==="td"||t==="th")?ue(e,n):r==="srcset"?Or(e,n):r==="style"?xe(n,Je()):t==="object"&&r==="data"?ue(e,n):n)}function vt(e,t,r){return(e==="video"||e==="audio")&&t==="autoplay"}function xr(e,t,r){try{if(typeof t=="string"){if(e.classList.contains(t))return!0}else for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}if(r)return e.matches(r)}catch{}return!1}function Ee(e,t,r){if(!e)return!1;if(e.nodeType!==e.ELEMENT_NODE)return r?Ee(e.parentNode,t,r):!1;for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}return r?Ee(e.parentNode,t,r):!1}function bt(e,t,r,n){try{const i=e.nodeType===e.ELEMENT_NODE?e:e.parentElement;if(i===null)return!1;if(typeof t=="string"){if(n){if(i.closest(`.${t}`))return!0}else if(i.classList.contains(t))return!0}else if(Ee(i,t,n))return!0;if(r){if(n){if(i.closest(r))return!0}else if(i.matches(r))return!0}}catch{}return!1}function Er(e,t,r){const n=e.contentWindow;if(!n)return;let i=!1,o;try{o=n.document.readyState}catch{return}if(o!=="complete"){const l=setTimeout(()=>{i||(t(),i=!0)},r);e.addEventListener("load",()=>{clearTimeout(l),i=!0,t()});return}const a="about:blank";if(n.location.href!==a||e.src===a||e.src==="")return setTimeout(t,0),e.addEventListener("load",t);e.addEventListener("load",t)}function kr(e,t,r){let n=!1,i;try{i=e.sheet}catch{return}if(i)return;const o=setTimeout(()=>{n||(t(),n=!0)},r);e.addEventListener("load",()=>{clearTimeout(o),n=!0,t()})}function Tr(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:a,inlineStylesheet:l,maskInputOptions:s={},maskTextFn:c,maskInputFn:u,dataURLOptions:p={},inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:h=!1}=t,y=Rr(r,n);switch(e.nodeType){case e.DOCUMENT_NODE:return e.compatMode!=="CSS1Compat"?{type:A.Document,childNodes:[],compatMode:e.compatMode}:{type:A.Document,childNodes:[]};case e.DOCUMENT_TYPE_NODE:return{type:A.DocumentType,name:e.name,publicId:e.publicId,systemId:e.systemId,rootId:y};case e.ELEMENT_NODE:return Dr(e,{doc:r,blockClass:i,blockSelector:o,inlineStylesheet:l,maskInputOptions:s,maskInputFn:u,dataURLOptions:p,inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:h,rootId:y});case e.TEXT_NODE:return Nr(e,{needsMask:a,maskTextFn:c,rootId:y});case e.CDATA_SECTION_NODE:return{type:A.CDATA,textContent:"",rootId:y};case e.COMMENT_NODE:return{type:A.Comment,textContent:e.textContent||"",rootId:y};default:return!1}}function Rr(e,t){if(!t.hasNode(e))return;const r=t.getId(e);return r===1?void 0:r}function Nr(e,t){var r;const{needsMask:n,maskTextFn:i,rootId:o}=t,a=e.parentNode&&e.parentNode.tagName;let l=e.textContent;const s=a==="STYLE"?!0:void 0,c=a==="SCRIPT"?!0:void 0;if(s&&l){try{e.nextSibling||e.previousSibling||!((r=e.parentNode.sheet)===null||r===void 0)&&r.cssRules&&(l=_e(e.parentNode.sheet))}catch(u){console.warn(`Cannot get CSS styles from text's parentNode. Error: ${u}`,e)}l=xe(l,Je())}return c&&(l="SCRIPT_PLACEHOLDER"),!s&&!c&&l&&n&&(l=i?i(l,e.parentElement):l.replace(/[\S]/g,"*")),{type:A.Text,textContent:l||"",isStyle:s,rootId:o}}function Dr(e,t){const{doc:r,blockClass:n,blockSelector:i,inlineStylesheet:o,maskInputOptions:a={},maskInputFn:l,dataURLOptions:s={},inlineImages:c,recordCanvas:u,keepIframeSrcFn:p,newlyAddedElement:m=!1,rootId:f}=t,g=xr(e,n,i),h=yr(e);let y={};const w=e.attributes.length;for(let S=0;SM.href===e.href);let b=null;S&&(b=_e(S)),b&&(delete y.rel,delete y.href,y._cssText=xe(b,S.href))}if(h==="style"&&e.sheet&&!(e.innerText||e.textContent||"").trim().length){const S=_e(e.sheet);S&&(y._cssText=xe(S,Je()))}if(h==="input"||h==="textarea"||h==="select"){const S=e.value,b=e.checked;y.type!=="radio"&&y.type!=="checkbox"&&y.type!=="submit"&&y.type!=="button"&&S?y.value=Ve({element:e,type:qe(e),tagName:h,value:S,maskInputOptions:a,maskInputFn:l}):b&&(y.checked=b)}if(h==="option"&&(e.selected&&!a.select?y.selected=!0:delete y.selected),h==="canvas"&&u){if(e.__context==="2d")pr(e)||(y.rr_dataURL=e.toDataURL(s.type,s.quality));else if(!("__context"in e)){const S=e.toDataURL(s.type,s.quality),b=document.createElement("canvas");b.width=e.width,b.height=e.height;const M=b.toDataURL(s.type,s.quality);S!==M&&(y.rr_dataURL=S)}}if(h==="img"&&c){ce||(ce=r.createElement("canvas"),yt=ce.getContext("2d"));const S=e,b=S.crossOrigin;S.crossOrigin="anonymous";const M=()=>{S.removeEventListener("load",M);try{ce.width=S.naturalWidth,ce.height=S.naturalHeight,yt.drawImage(S,0,0),y.rr_dataURL=ce.toDataURL(s.type,s.quality)}catch(F){console.warn(`Cannot inline img src=${S.currentSrc}! Error: ${F}`)}b?y.crossOrigin=b:S.removeAttribute("crossorigin")};S.complete&&S.naturalWidth!==0?M():S.addEventListener("load",M)}if(h==="audio"||h==="video"){const S=y;S.rr_mediaState=e.paused?"paused":"played",S.rr_mediaCurrentTime=e.currentTime,S.rr_mediaPlaybackRate=e.playbackRate,S.rr_mediaMuted=e.muted,S.rr_mediaLoop=e.loop,S.rr_mediaVolume=e.volume}if(m||(e.scrollLeft&&(y.rr_scrollLeft=e.scrollLeft),e.scrollTop&&(y.rr_scrollTop=e.scrollTop)),g){const{width:S,height:b}=e.getBoundingClientRect();y={class:y.class,rr_width:`${S}px`,rr_height:`${b}px`}}h==="iframe"&&!p(y.src)&&(e.contentDocument||(y.rr_src=y.src),delete y.src);let v;try{customElements.get(h)&&(v=!0)}catch{}return{type:A.Element,tagName:h,attributes:y,childNodes:[],isSVG:_r(e)||void 0,needBlock:g,rootId:f,isCustom:v}}function E(e){return e==null?"":e.toLowerCase()}function Ar(e,t){if(t.comment&&e.type===A.Comment)return!0;if(e.type===A.Element){if(t.script&&(e.tagName==="script"||e.tagName==="link"&&(e.attributes.rel==="preload"||e.attributes.rel==="modulepreload")&&e.attributes.as==="script"||e.tagName==="link"&&e.attributes.rel==="prefetch"&&typeof e.attributes.href=="string"&&mt(e.attributes.href)==="js"))return!0;if(t.headFavicon&&(e.tagName==="link"&&e.attributes.rel==="shortcut icon"||e.tagName==="meta"&&(E(e.attributes.name).match(/^msapplication-tile(image|color)$/)||E(e.attributes.name)==="application-name"||E(e.attributes.rel)==="icon"||E(e.attributes.rel)==="apple-touch-icon"||E(e.attributes.rel)==="shortcut icon")))return!0;if(e.tagName==="meta"){if(t.headMetaDescKeywords&&E(e.attributes.name).match(/^description|keywords$/))return!0;if(t.headMetaSocial&&(E(e.attributes.property).match(/^(og|twitter|fb):/)||E(e.attributes.name).match(/^(og|twitter):/)||E(e.attributes.name)==="pinterest"))return!0;if(t.headMetaRobots&&(E(e.attributes.name)==="robots"||E(e.attributes.name)==="googlebot"||E(e.attributes.name)==="bingbot"))return!0;if(t.headMetaHttpEquiv&&e.attributes["http-equiv"]!==void 0)return!0;if(t.headMetaAuthorship&&(E(e.attributes.name)==="author"||E(e.attributes.name)==="generator"||E(e.attributes.name)==="framework"||E(e.attributes.name)==="publisher"||E(e.attributes.name)==="progid"||E(e.attributes.property).match(/^article:/)||E(e.attributes.property).match(/^product:/)))return!0;if(t.headMetaVerification&&(E(e.attributes.name)==="google-site-verification"||E(e.attributes.name)==="yandex-verification"||E(e.attributes.name)==="csrf-token"||E(e.attributes.name)==="p:domain_verify"||E(e.attributes.name)==="verify-v1"||E(e.attributes.name)==="verification"||E(e.attributes.name)==="shopify-checkout-api-token"))return!0}}return!1}function de(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,maskTextClass:a,maskTextSelector:l,skipChild:s=!1,inlineStylesheet:c=!0,maskInputOptions:u={},maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g={},inlineImages:h=!1,recordCanvas:y=!1,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S=5e3,onStylesheetLoad:b,stylesheetLoadTimeout:M=5e3,keepIframeSrcFn:F=()=>!1,newlyAddedElement:P=!1}=t;let{needsMask:k}=t,{preserveWhiteSpace:T=!0}=t;!k&&e.childNodes&&(k=bt(e,a,l,k===void 0));const j=Tr(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,dataURLOptions:g,inlineImages:h,recordCanvas:y,keepIframeSrcFn:F,newlyAddedElement:P});if(!j)return console.warn(e,"not serialized"),null;let V;n.hasNode(e)?V=n.getId(e):Ar(j,f)||!T&&j.type===A.Text&&!j.isStyle&&!j.textContent.replace(/^\s+|\s+$/gm,"").length?V=Se:V=gt();const x=Object.assign(j,{id:V});if(n.add(e,x),V===Se)return null;w&&w(e);let oe=!s;if(x.type===A.Element){oe=oe&&!x.needBlock,delete x.needBlock;const H=e.shadowRoot;H&&ye(H)&&(x.isShadowHost=!0)}if((x.type===A.Document||x.type===A.Element)&&oe){f.headWhitespace&&x.type===A.Element&&x.tagName==="head"&&(T=!1);const H={doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:s,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F};if(!(x.type===A.Element&&x.tagName==="textarea"&&x.attributes.value!==void 0))for(const ee of Array.from(e.childNodes)){const K=de(ee,H);K&&x.childNodes.push(K)}if(ar(e)&&e.shadowRoot)for(const ee of Array.from(e.shadowRoot.childNodes)){const K=de(ee,H);K&&(ye(e.shadowRoot)&&(K.isShadow=!0),x.childNodes.push(K))}}return e.parentNode&&ge(e.parentNode)&&ye(e.parentNode)&&(x.isShadow=!0),x.type===A.Element&&x.tagName==="iframe"&&Er(e,()=>{const H=e.contentDocument;if(H&&v){const ee=de(H,{doc:H,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F});ee&&v(e,ee)}},S),x.type===A.Element&&x.tagName==="link"&&typeof x.attributes.rel=="string"&&(x.attributes.rel==="stylesheet"||x.attributes.rel==="preload"&&typeof x.attributes.href=="string"&&mt(x.attributes.href)==="css")&&kr(e,()=>{if(b){const H=de(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F});H&&b(e,H)}},M),x}function Lr(e,t){const{mirror:r=new ht,blockClass:n="rr-block",blockSelector:i=null,maskTextClass:o="rr-mask",maskTextSelector:a=null,inlineStylesheet:l=!0,inlineImages:s=!1,recordCanvas:c=!1,maskAllInputs:u=!1,maskTextFn:p,maskInputFn:m,slimDOM:f=!1,dataURLOptions:g,preserveWhiteSpace:h,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:v,onStylesheetLoad:S,stylesheetLoadTimeout:b,keepIframeSrcFn:M=()=>!1}=t||{};return de(e,{doc:e,mirror:r,blockClass:n,blockSelector:i,maskTextClass:o,maskTextSelector:a,skipChild:!1,inlineStylesheet:l,maskInputOptions:u===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:u===!1?{password:!0}:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:f==="all",headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0}:f===!1?{}:f,dataURLOptions:g,inlineImages:s,recordCanvas:c,preserveWhiteSpace:h,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:v,onStylesheetLoad:S,stylesheetLoadTimeout:b,keepIframeSrcFn:M,newlyAddedElement:!1})}function W(e,t,r=document){const n={capture:!0,passive:!0};return r.addEventListener(e,t,n),()=>r.removeEventListener(e,t,n)}const fe=`Please stop import mirror directly. Instead of that,\r now you can use replayer.getMirror() to access the mirror instance of a replayer,\r or you can use record.mirror to access the mirror instance during recording.`;let wt={map:{},getId(){return console.error(fe),-1},getNode(){return console.error(fe),null},removeNodeFromMap(){console.error(fe)},has(){return console.error(fe),!1},reset(){console.error(fe)}};typeof window<"u"&&window.Proxy&&window.Reflect&&(wt=new Proxy(wt,{get(e,t,r){return t==="map"&&console.error(fe),Reflect.get(e,t,r)}}));function ve(e,t,r={}){let n=null,i=0;return function(...o){const a=Date.now();!i&&r.leading===!1&&(i=a);const l=t-(a-i),s=this;l<=0||l>t?(n&&(clearTimeout(n),n=null),i=a,e.apply(s,o)):!n&&r.trailing!==!1&&(n=setTimeout(()=>{i=r.leading===!1?0:Date.now(),n=null,e.apply(s,o)},l))}}function ke(e,t,r,n,i=window){const o=i.Object.getOwnPropertyDescriptor(e,t);return i.Object.defineProperty(e,t,n?r:{set(a){setTimeout(()=>{r.set.call(this,a)},0),o&&o.set&&o.set.call(this,a)}}),()=>ke(e,t,o||{},!0)}function he(e,t,r){try{if(!(t in e))return()=>{};const n=e[t],i=r(n);return typeof i=="function"&&(i.prototype=i.prototype||{},Object.defineProperties(i,{__rrweb_original__:{enumerable:!1,value:n}})),e[t]=i,()=>{e[t]=n}}catch{return()=>{}}}let Te=Date.now;/[1-9][0-9]{12}/.test(Date.now().toString())||(Te=()=>new Date().getTime());function Mt(e){var t,r,n,i,o,a;const l=e.document;return{left:l.scrollingElement?l.scrollingElement.scrollLeft:e.pageXOffset!==void 0?e.pageXOffset:l?.documentElement.scrollLeft||((r=(t=l?.body)===null||t===void 0?void 0:t.parentElement)===null||r===void 0?void 0:r.scrollLeft)||((n=l?.body)===null||n===void 0?void 0:n.scrollLeft)||0,top:l.scrollingElement?l.scrollingElement.scrollTop:e.pageYOffset!==void 0?e.pageYOffset:l?.documentElement.scrollTop||((o=(i=l?.body)===null||i===void 0?void 0:i.parentElement)===null||o===void 0?void 0:o.scrollTop)||((a=l?.body)===null||a===void 0?void 0:a.scrollTop)||0}}function It(){return window.innerHeight||document.documentElement&&document.documentElement.clientHeight||document.body&&document.body.clientHeight}function Ct(){return window.innerWidth||document.documentElement&&document.documentElement.clientWidth||document.body&&document.body.clientWidth}function Ot(e){return e?e.nodeType===e.ELEMENT_NODE?e:e.parentElement:null}function U(e,t,r,n){if(!e)return!1;const i=Ot(e);if(!i)return!1;try{if(typeof t=="string"){if(i.classList.contains(t)||n&&i.closest("."+t)!==null)return!0}else if(Ee(i,t,n))return!0}catch{}return!!(r&&(i.matches(r)||n&&i.closest(r)!==null))}function Pr(e,t){return t.getId(e)!==-1}function Xe(e,t){return t.getId(e)===Se}function _t(e,t){if(ge(e))return!1;const r=t.getId(e);return t.has(r)?e.parentNode&&e.parentNode.nodeType===e.DOCUMENT_NODE?!1:e.parentNode?_t(e.parentNode,t):!0:!0}function Ke(e){return!!e.changedTouches}function Fr(e=window){"NodeList"in e&&!e.NodeList.prototype.forEach&&(e.NodeList.prototype.forEach=Array.prototype.forEach),"DOMTokenList"in e&&!e.DOMTokenList.prototype.forEach&&(e.DOMTokenList.prototype.forEach=Array.prototype.forEach),Node.prototype.contains||(Node.prototype.contains=(...t)=>{let r=t[0];if(!(0 in t))throw new TypeError("1 argument is required");do if(this===r)return!0;while(r=r&&r.parentNode);return!1})}function xt(e,t){return!!(e.nodeName==="IFRAME"&&t.getMeta(e))}function Et(e,t){return!!(e.nodeName==="LINK"&&e.nodeType===e.ELEMENT_NODE&&e.getAttribute&&e.getAttribute("rel")==="stylesheet"&&t.getMeta(e))}function Ye(e){return!!e?.shadowRoot}class Br{constructor(){this.id=1,this.styleIDMap=new WeakMap,this.idStyleMap=new Map}getId(t){var r;return(r=this.styleIDMap.get(t))!==null&&r!==void 0?r:-1}has(t){return this.styleIDMap.has(t)}add(t,r){if(this.has(t))return this.getId(t);let n;return r===void 0?n=this.id++:n=r,this.styleIDMap.set(t,n),this.idStyleMap.set(n,t),n}getStyle(t){return this.idStyleMap.get(t)||null}reset(){this.styleIDMap=new WeakMap,this.idStyleMap=new Map,this.id=1}generateId(){return this.id++}}function kt(e){var t,r;let n=null;return((r=(t=e.getRootNode)===null||t===void 0?void 0:t.call(e))===null||r===void 0?void 0:r.nodeType)===Node.DOCUMENT_FRAGMENT_NODE&&e.getRootNode().host&&(n=e.getRootNode().host),n}function Wr(e){let t=e,r;for(;r=kt(t);)t=r;return t}function Ur(e){const t=e.ownerDocument;if(!t)return!1;const r=Wr(e);return t.contains(r)}function Tt(e){const t=e.ownerDocument;return t?t.contains(e)||Ur(e):!1}var _=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(_||{}),C=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(C||{}),$=(e=>(e[e.MouseUp=0]="MouseUp",e[e.MouseDown=1]="MouseDown",e[e.Click=2]="Click",e[e.ContextMenu=3]="ContextMenu",e[e.DblClick=4]="DblClick",e[e.Focus=5]="Focus",e[e.Blur=6]="Blur",e[e.TouchStart=7]="TouchStart",e[e.TouchMove_Departed=8]="TouchMove_Departed",e[e.TouchEnd=9]="TouchEnd",e[e.TouchCancel=10]="TouchCancel",e))($||{}),Y=(e=>(e[e.Mouse=0]="Mouse",e[e.Pen=1]="Pen",e[e.Touch=2]="Touch",e))(Y||{}),pe=(e=>(e[e["2D"]=0]="2D",e[e.WebGL=1]="WebGL",e[e.WebGL2=2]="WebGL2",e))(pe||{});function Rt(e){return"__ln"in e}class Hr{constructor(){this.length=0,this.head=null,this.tail=null}get(t){if(t>=this.length)throw new Error("Position outside of list range");let r=this.head;for(let n=0;n`${e}@${t}`;class zr{constructor(){this.frozen=!1,this.locked=!1,this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.mapRemoves=[],this.movedMap={},this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.processMutations=t=>{t.forEach(this.processMutation),this.emit()},this.emit=()=>{if(this.frozen||this.locked)return;const t=[],r=new Set,n=new Hr,i=s=>{let c=s,u=Se;for(;u===Se;)c=c&&c.nextSibling,u=c&&this.mirror.getId(c);return u},o=s=>{if(!s.parentNode||!Tt(s)||s.parentNode.tagName==="TEXTAREA")return;const c=ge(s.parentNode)?this.mirror.getId(kt(s)):this.mirror.getId(s.parentNode),u=i(s);if(c===-1||u===-1)return n.addNode(s);const p=de(s,{doc:this.doc,mirror:this.mirror,blockClass:this.blockClass,blockSelector:this.blockSelector,maskTextClass:this.maskTextClass,maskTextSelector:this.maskTextSelector,skipChild:!0,newlyAddedElement:!0,inlineStylesheet:this.inlineStylesheet,maskInputOptions:this.maskInputOptions,maskTextFn:this.maskTextFn,maskInputFn:this.maskInputFn,slimDOMOptions:this.slimDOMOptions,dataURLOptions:this.dataURLOptions,recordCanvas:this.recordCanvas,inlineImages:this.inlineImages,onSerialize:m=>{xt(m,this.mirror)&&this.iframeManager.addIframe(m),Et(m,this.mirror)&&this.stylesheetManager.trackLinkElement(m),Ye(s)&&this.shadowDomManager.addShadowRoot(s.shadowRoot,this.doc)},onIframeLoad:(m,f)=>{this.iframeManager.attachIframe(m,f),this.shadowDomManager.observeAttachShadow(m)},onStylesheetLoad:(m,f)=>{this.stylesheetManager.attachLinkElement(m,f)}});p&&(t.push({parentId:c,nextId:u,node:p}),r.add(p.id))};for(;this.mapRemoves.length;)this.mirror.removeNodeFromMap(this.mapRemoves.shift());for(const s of this.movedSet)Dt(this.removes,s,this.mirror)&&!this.movedSet.has(s.parentNode)||o(s);for(const s of this.addedSet)!Lt(this.droppedSet,s)&&!Dt(this.removes,s,this.mirror)||Lt(this.movedSet,s)?o(s):this.droppedSet.add(s);let a=null;for(;n.length;){let s=null;if(a){const c=this.mirror.getId(a.value.parentNode),u=i(a.value);c!==-1&&u!==-1&&(s=a)}if(!s){let c=n.tail;for(;c;){const u=c;if(c=c.previous,u){const p=this.mirror.getId(u.value.parentNode);if(i(u.value)===-1)continue;if(p!==-1){s=u;break}else{const f=u.value;if(f.parentNode&&f.parentNode.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const g=f.parentNode.host;if(this.mirror.getId(g)!==-1){s=u;break}}}}}}if(!s){for(;n.head;)n.removeNode(n.head.value);break}a=s.previous,n.removeNode(s.value),o(s.value)}const l={texts:this.texts.map(s=>{const c=s.node;return c.parentNode&&c.parentNode.tagName==="TEXTAREA"&&this.genTextAreaValueMutation(c.parentNode),{id:this.mirror.getId(c),value:s.value}}).filter(s=>!r.has(s.id)).filter(s=>this.mirror.has(s.id)),attributes:this.attributes.map(s=>{const{attributes:c}=s;if(typeof c.style=="string"){const u=JSON.stringify(s.styleDiff),p=JSON.stringify(s._unchangedStyles);u.length!r.has(s.id)).filter(s=>this.mirror.has(s.id)),removes:this.removes,adds:t};!l.texts.length&&!l.attributes.length&&!l.removes.length&&!l.adds.length||(this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.movedMap={},this.mutationCb(l))},this.genTextAreaValueMutation=t=>{let r=this.attributeMap.get(t);r||(r={node:t,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(r),this.attributeMap.set(t,r)),r.attributes.value=Array.from(t.childNodes,n=>n.textContent||"").join("")},this.processMutation=t=>{if(!Xe(t.target,this.mirror))switch(t.type){case"characterData":{const r=t.target.textContent;!U(t.target,this.blockClass,this.blockSelector,!1)&&r!==t.oldValue&&this.texts.push({value:bt(t.target,this.maskTextClass,this.maskTextSelector,!0)&&r?this.maskTextFn?this.maskTextFn(r,Ot(t.target)):r.replace(/[\S]/g,"*"):r,node:t.target});break}case"attributes":{const r=t.target;let n=t.attributeName,i=t.target.getAttribute(n);if(n==="value"){const a=qe(r);i=Ve({element:r,maskInputOptions:this.maskInputOptions,tagName:r.tagName,type:a,value:i,maskInputFn:this.maskInputFn})}if(U(t.target,this.blockClass,this.blockSelector,!1)||i===t.oldValue)return;let o=this.attributeMap.get(t.target);if(r.tagName==="IFRAME"&&n==="src"&&!this.keepIframeSrcFn(i))if(!r.contentDocument)n="rr_src";else return;if(o||(o={node:t.target,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(o),this.attributeMap.set(t.target,o)),n==="type"&&r.tagName==="INPUT"&&(t.oldValue||"").toLowerCase()==="password"&&r.setAttribute("data-rr-is-password","true"),!vt(r.tagName,n)&&(o.attributes[n]=St(this.doc,te(r.tagName),te(n),i),n==="style")){if(!this.unattachedDoc)try{this.unattachedDoc=document.implementation.createHTMLDocument()}catch{this.unattachedDoc=this.doc}const a=this.unattachedDoc.createElement("span");t.oldValue&&a.setAttribute("style",t.oldValue);for(const l of Array.from(r.style)){const s=r.style.getPropertyValue(l),c=r.style.getPropertyPriority(l);s!==a.style.getPropertyValue(l)||c!==a.style.getPropertyPriority(l)?c===""?o.styleDiff[l]=s:o.styleDiff[l]=[s,c]:o._unchangedStyles[l]=[s,c]}for(const l of Array.from(a.style))r.style.getPropertyValue(l)===""&&(o.styleDiff[l]=!1)}break}case"childList":{if(U(t.target,this.blockClass,this.blockSelector,!0))return;if(t.target.tagName==="TEXTAREA"){this.genTextAreaValueMutation(t.target);return}t.addedNodes.forEach(r=>this.genAdds(r,t.target)),t.removedNodes.forEach(r=>{const n=this.mirror.getId(r),i=ge(t.target)?this.mirror.getId(t.target.host):this.mirror.getId(t.target);U(t.target,this.blockClass,this.blockSelector,!1)||Xe(r,this.mirror)||!Pr(r,this.mirror)||(this.addedSet.has(r)?(Qe(this.addedSet,r),this.droppedSet.add(r)):this.addedSet.has(t.target)&&n===-1||_t(t.target,this.mirror)||(this.movedSet.has(r)&&this.movedMap[Nt(n,i)]?Qe(this.movedSet,r):this.removes.push({parentId:i,id:n,isShadow:ge(t.target)&&ye(t.target)?!0:void 0})),this.mapRemoves.push(r))});break}}},this.genAdds=(t,r)=>{if(!this.processedNodeManager.inOtherBuffer(t,this)&&!(this.addedSet.has(t)||this.movedSet.has(t))){if(this.mirror.hasNode(t)){if(Xe(t,this.mirror))return;this.movedSet.add(t);let n=null;r&&this.mirror.hasNode(r)&&(n=this.mirror.getId(r)),n&&n!==-1&&(this.movedMap[Nt(this.mirror.getId(t),n)]=!0)}else this.addedSet.add(t),this.droppedSet.delete(t);U(t,this.blockClass,this.blockSelector,!1)||(t.childNodes.forEach(n=>this.genAdds(n)),Ye(t)&&t.shadowRoot.childNodes.forEach(n=>{this.processedNodeManager.add(n,this),this.genAdds(n,t)}))}}}init(t){["mutationCb","blockClass","blockSelector","maskTextClass","maskTextSelector","inlineStylesheet","maskInputOptions","maskTextFn","maskInputFn","keepIframeSrcFn","recordCanvas","inlineImages","slimDOMOptions","dataURLOptions","doc","mirror","iframeManager","stylesheetManager","shadowDomManager","canvasManager","processedNodeManager"].forEach(r=>{this[r]=t[r]})}freeze(){this.frozen=!0,this.canvasManager.freeze()}unfreeze(){this.frozen=!1,this.canvasManager.unfreeze(),this.emit()}isFrozen(){return this.frozen}lock(){this.locked=!0,this.canvasManager.lock()}unlock(){this.locked=!1,this.canvasManager.unlock(),this.emit()}reset(){this.shadowDomManager.reset(),this.canvasManager.reset()}}function Qe(e,t){e.delete(t),t.childNodes.forEach(r=>Qe(e,r))}function Dt(e,t,r){return e.length===0?!1:At(e,t,r)}function At(e,t,r){const{parentNode:n}=t;if(!n)return!1;const i=r.getId(n);return e.some(o=>o.id===i)?!0:At(e,n,r)}function Lt(e,t){return e.size===0?!1:Pt(e,t)}function Pt(e,t){const{parentNode:r}=t;return r?e.has(r)?!0:Pt(e,r):!1}let be;function $r(e){be=e}function Gr(){be=void 0}const O=e=>be?(...r)=>{try{return e(...r)}catch(n){if(be&&be(n)===!0)return;throw n}}:e,re=[];function we(e){try{if("composedPath"in e){const t=e.composedPath();if(t.length)return t[0]}else if("path"in e&&e.path.length)return e.path[0]}catch{}return e&&e.target}function Ft(e,t){var r,n;const i=new zr;re.push(i),i.init(e);let o=window.MutationObserver||window.__rrMutationObserver;const a=(n=(r=window?.Zone)===null||r===void 0?void 0:r.__symbol__)===null||n===void 0?void 0:n.call(r,"MutationObserver");a&&window[a]&&(o=window[a]);const l=new o(O(i.processMutations.bind(i)));return l.observe(t,{attributes:!0,attributeOldValue:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0}),l}function jr({mousemoveCb:e,sampling:t,doc:r,mirror:n}){if(t.mousemove===!1)return()=>{};const i=typeof t.mousemove=="number"?t.mousemove:50,o=typeof t.mousemoveCallback=="number"?t.mousemoveCallback:500;let a=[],l;const s=ve(O(p=>{const m=Date.now()-l;e(a.map(f=>(f.timeOffset-=m,f)),p),a=[],l=null}),o),c=O(ve(O(p=>{const m=we(p),{clientX:f,clientY:g}=Ke(p)?p.changedTouches[0]:p;l||(l=Te()),a.push({x:f,y:g,id:n.getId(m),timeOffset:Te()-l}),s(typeof DragEvent<"u"&&p instanceof DragEvent?C.Drag:p instanceof MouseEvent?C.MouseMove:C.TouchMove)}),i,{trailing:!1})),u=[W("mousemove",c,r),W("touchmove",c,r),W("drag",c,r)];return O(()=>{u.forEach(p=>p())})}function Vr({mouseInteractionCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){if(o.mouseInteraction===!1)return()=>{};const a=o.mouseInteraction===!0||o.mouseInteraction===void 0?{}:o.mouseInteraction,l=[];let s=null;const c=u=>p=>{const m=we(p);if(U(m,n,i,!0))return;let f=null,g=u;if("pointerType"in p){switch(p.pointerType){case"mouse":f=Y.Mouse;break;case"touch":f=Y.Touch;break;case"pen":f=Y.Pen;break}f===Y.Touch?$[u]===$.MouseDown?g="TouchStart":$[u]===$.MouseUp&&(g="TouchEnd"):Y.Pen}else Ke(p)&&(f=Y.Touch);f!==null?(s=f,(g.startsWith("Touch")&&f===Y.Touch||g.startsWith("Mouse")&&f===Y.Mouse)&&(f=null)):$[u]===$.Click&&(f=s,s=null);const h=Ke(p)?p.changedTouches[0]:p;if(!h)return;const y=r.getId(m),{clientX:w,clientY:v}=h;O(e)(Object.assign({type:$[g],id:y,x:w,y:v},f!==null&&{pointerType:f}))};return Object.keys($).filter(u=>Number.isNaN(Number(u))&&!u.endsWith("_Departed")&&a[u]!==!1).forEach(u=>{let p=te(u);const m=c(u);if(window.PointerEvent)switch($[u]){case $.MouseDown:case $.MouseUp:p=p.replace("mouse","pointer");break;case $.TouchStart:case $.TouchEnd:return}l.push(W(p,m,t))}),O(()=>{l.forEach(u=>u())})}function Bt({scrollCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){const a=O(ve(O(l=>{const s=we(l);if(!s||U(s,n,i,!0))return;const c=r.getId(s);if(s===t&&t.defaultView){const u=Mt(t.defaultView);e({id:c,x:u.left,y:u.top})}else e({id:c,x:s.scrollLeft,y:s.scrollTop})}),o.scroll||100));return W("scroll",a,t)}function qr({viewportResizeCb:e},{win:t}){let r=-1,n=-1;const i=O(ve(O(()=>{const o=It(),a=Ct();(r!==o||n!==a)&&(e({width:Number(a),height:Number(o)}),r=o,n=a)}),200));return W("resize",i,t)}const Jr=["INPUT","TEXTAREA","SELECT"],Wt=new WeakMap;function Xr({inputCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,ignoreClass:o,ignoreSelector:a,maskInputOptions:l,maskInputFn:s,sampling:c,userTriggeredOnInput:u}){function p(v){let S=we(v);const b=v.isTrusted,M=S&&S.tagName;if(S&&M==="OPTION"&&(S=S.parentElement),!S||!M||Jr.indexOf(M)<0||U(S,n,i,!0)||S.classList.contains(o)||a&&S.matches(a))return;let F=S.value,P=!1;const k=qe(S)||"";k==="radio"||k==="checkbox"?P=S.checked:(l[M.toLowerCase()]||l[k])&&(F=Ve({element:S,maskInputOptions:l,tagName:M,type:k,value:F,maskInputFn:s})),m(S,u?{text:F,isChecked:P,userTriggered:b}:{text:F,isChecked:P});const T=S.name;k==="radio"&&T&&P&&t.querySelectorAll(`input[type="radio"][name="${T}"]`).forEach(j=>{if(j!==S){const V=j.value;m(j,u?{text:V,isChecked:!P,userTriggered:!1}:{text:V,isChecked:!P})}})}function m(v,S){const b=Wt.get(v);if(!b||b.text!==S.text||b.isChecked!==S.isChecked){Wt.set(v,S);const M=r.getId(v);O(e)(Object.assign(Object.assign({},S),{id:M}))}}const g=(c.input==="last"?["change"]:["input","change"]).map(v=>W(v,O(p),t)),h=t.defaultView;if(!h)return()=>{g.forEach(v=>v())};const y=h.Object.getOwnPropertyDescriptor(h.HTMLInputElement.prototype,"value"),w=[[h.HTMLInputElement.prototype,"value"],[h.HTMLInputElement.prototype,"checked"],[h.HTMLSelectElement.prototype,"value"],[h.HTMLTextAreaElement.prototype,"value"],[h.HTMLSelectElement.prototype,"selectedIndex"],[h.HTMLOptionElement.prototype,"selected"]];return y&&y.set&&g.push(...w.map(v=>ke(v[0],v[1],{set(){O(p)({target:this,isTrusted:!1})}},!1,h))),O(()=>{g.forEach(v=>v())})}function Re(e){const t=[];function r(n,i){if(Ne("CSSGroupingRule")&&n.parentRule instanceof CSSGroupingRule||Ne("CSSMediaRule")&&n.parentRule instanceof CSSMediaRule||Ne("CSSSupportsRule")&&n.parentRule instanceof CSSSupportsRule||Ne("CSSConditionRule")&&n.parentRule instanceof CSSConditionRule){const a=Array.from(n.parentRule.cssRules).indexOf(n);i.unshift(a)}else if(n.parentStyleSheet){const a=Array.from(n.parentStyleSheet.cssRules).indexOf(n);i.unshift(a)}return i}return r(e,t)}function Z(e,t,r){let n,i;return e?(e.ownerNode?n=t.getId(e.ownerNode):i=r.getId(e),{styleId:i,id:n}):{}}function Kr({styleSheetRuleCb:e,mirror:t,stylesheetManager:r},{win:n}){if(!n.CSSStyleSheet||!n.CSSStyleSheet.prototype)return()=>{};const i=n.CSSStyleSheet.prototype.insertRule;n.CSSStyleSheet.prototype.insertRule=new Proxy(i,{apply:O((u,p,m)=>{const[f,g]=m,{id:h,styleId:y}=Z(p,t,r.styleMirror);return(h&&h!==-1||y&&y!==-1)&&e({id:h,styleId:y,adds:[{rule:f,index:g}]}),u.apply(p,m)})});const o=n.CSSStyleSheet.prototype.deleteRule;n.CSSStyleSheet.prototype.deleteRule=new Proxy(o,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,removes:[{index:f}]}),u.apply(p,m)})});let a;n.CSSStyleSheet.prototype.replace&&(a=n.CSSStyleSheet.prototype.replace,n.CSSStyleSheet.prototype.replace=new Proxy(a,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,replace:f}),u.apply(p,m)})}));let l;n.CSSStyleSheet.prototype.replaceSync&&(l=n.CSSStyleSheet.prototype.replaceSync,n.CSSStyleSheet.prototype.replaceSync=new Proxy(l,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,replaceSync:f}),u.apply(p,m)})}));const s={};De("CSSGroupingRule")?s.CSSGroupingRule=n.CSSGroupingRule:(De("CSSMediaRule")&&(s.CSSMediaRule=n.CSSMediaRule),De("CSSConditionRule")&&(s.CSSConditionRule=n.CSSConditionRule),De("CSSSupportsRule")&&(s.CSSSupportsRule=n.CSSSupportsRule));const c={};return Object.entries(s).forEach(([u,p])=>{c[u]={insertRule:p.prototype.insertRule,deleteRule:p.prototype.deleteRule},p.prototype.insertRule=new Proxy(c[u].insertRule,{apply:O((m,f,g)=>{const[h,y]=g,{id:w,styleId:v}=Z(f.parentStyleSheet,t,r.styleMirror);return(w&&w!==-1||v&&v!==-1)&&e({id:w,styleId:v,adds:[{rule:h,index:[...Re(f),y||0]}]}),m.apply(f,g)})}),p.prototype.deleteRule=new Proxy(c[u].deleteRule,{apply:O((m,f,g)=>{const[h]=g,{id:y,styleId:w}=Z(f.parentStyleSheet,t,r.styleMirror);return(y&&y!==-1||w&&w!==-1)&&e({id:y,styleId:w,removes:[{index:[...Re(f),h]}]}),m.apply(f,g)})})}),O(()=>{n.CSSStyleSheet.prototype.insertRule=i,n.CSSStyleSheet.prototype.deleteRule=o,a&&(n.CSSStyleSheet.prototype.replace=a),l&&(n.CSSStyleSheet.prototype.replaceSync=l),Object.entries(s).forEach(([u,p])=>{p.prototype.insertRule=c[u].insertRule,p.prototype.deleteRule=c[u].deleteRule})})}function Ut({mirror:e,stylesheetManager:t},r){var n,i,o;let a=null;r.nodeName==="#document"?a=e.getId(r):a=e.getId(r.host);const l=r.nodeName==="#document"?(n=r.defaultView)===null||n===void 0?void 0:n.Document:(o=(i=r.ownerDocument)===null||i===void 0?void 0:i.defaultView)===null||o===void 0?void 0:o.ShadowRoot,s=l?.prototype?Object.getOwnPropertyDescriptor(l?.prototype,"adoptedStyleSheets"):void 0;return a===null||a===-1||!l||!s?()=>{}:(Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get(){var c;return(c=s.get)===null||c===void 0?void 0:c.call(this)},set(c){var u;const p=(u=s.set)===null||u===void 0?void 0:u.call(this,c);if(a!==null&&a!==-1)try{t.adoptStyleSheets(c,a)}catch{}return p}}),O(()=>{Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get:s.get,set:s.set})}))}function Yr({styleDeclarationCb:e,mirror:t,ignoreCSSAttributes:r,stylesheetManager:n},{win:i}){const o=i.CSSStyleDeclaration.prototype.setProperty;i.CSSStyleDeclaration.prototype.setProperty=new Proxy(o,{apply:O((l,s,c)=>{var u;const[p,m,f]=c;if(r.has(p))return o.apply(s,[p,m,f]);const{id:g,styleId:h}=Z((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,set:{property:p,value:m,priority:f},index:Re(s.parentRule)}),l.apply(s,c)})});const a=i.CSSStyleDeclaration.prototype.removeProperty;return i.CSSStyleDeclaration.prototype.removeProperty=new Proxy(a,{apply:O((l,s,c)=>{var u;const[p]=c;if(r.has(p))return a.apply(s,[p]);const{id:m,styleId:f}=Z((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(m&&m!==-1||f&&f!==-1)&&e({id:m,styleId:f,remove:{property:p},index:Re(s.parentRule)}),l.apply(s,c)})}),O(()=>{i.CSSStyleDeclaration.prototype.setProperty=o,i.CSSStyleDeclaration.prototype.removeProperty=a})}function Qr({mediaInteractionCb:e,blockClass:t,blockSelector:r,mirror:n,sampling:i,doc:o}){const a=O(s=>ve(O(c=>{const u=we(c);if(!u||U(u,t,r,!0))return;const{currentTime:p,volume:m,muted:f,playbackRate:g,loop:h}=u;e({type:s,id:n.getId(u),currentTime:p,volume:m,muted:f,playbackRate:g,loop:h})}),i.media||500)),l=[W("play",a(0),o),W("pause",a(1),o),W("seeked",a(2),o),W("volumechange",a(3),o),W("ratechange",a(4),o)];return O(()=>{l.forEach(s=>s())})}function Zr({fontCb:e,doc:t}){const r=t.defaultView;if(!r)return()=>{};const n=[],i=new WeakMap,o=r.FontFace;r.FontFace=function(s,c,u){const p=new o(s,c,u);return i.set(p,{family:s,buffer:typeof c!="string",descriptors:u,fontSource:typeof c=="string"?c:JSON.stringify(Array.from(new Uint8Array(c)))}),p};const a=he(t.fonts,"add",function(l){return function(s){return setTimeout(O(()=>{const c=i.get(s);c&&(e(c),i.delete(s))}),0),l.apply(this,[s])}});return n.push(()=>{r.FontFace=o}),n.push(a),O(()=>{n.forEach(l=>l())})}function en(e){const{doc:t,mirror:r,blockClass:n,blockSelector:i,selectionCb:o}=e;let a=!0;const l=O(()=>{const s=t.getSelection();if(!s||a&&s?.isCollapsed)return;a=s.isCollapsed||!1;const c=[],u=s.rangeCount||0;for(let p=0;p{}:he(r.customElements,"define",function(i){return function(o,a,l){try{t({define:{name:o}})}catch{console.warn(`Custom element callback failed for ${o}`)}return i.apply(this,[o,a,l])}})}function rn(e,t){const{mutationCb:r,mousemoveCb:n,mouseInteractionCb:i,scrollCb:o,viewportResizeCb:a,inputCb:l,mediaInteractionCb:s,styleSheetRuleCb:c,styleDeclarationCb:u,canvasMutationCb:p,fontCb:m,selectionCb:f,customElementCb:g}=e;e.mutationCb=(...h)=>{t.mutation&&t.mutation(...h),r(...h)},e.mousemoveCb=(...h)=>{t.mousemove&&t.mousemove(...h),n(...h)},e.mouseInteractionCb=(...h)=>{t.mouseInteraction&&t.mouseInteraction(...h),i(...h)},e.scrollCb=(...h)=>{t.scroll&&t.scroll(...h),o(...h)},e.viewportResizeCb=(...h)=>{t.viewportResize&&t.viewportResize(...h),a(...h)},e.inputCb=(...h)=>{t.input&&t.input(...h),l(...h)},e.mediaInteractionCb=(...h)=>{t.mediaInteaction&&t.mediaInteaction(...h),s(...h)},e.styleSheetRuleCb=(...h)=>{t.styleSheetRule&&t.styleSheetRule(...h),c(...h)},e.styleDeclarationCb=(...h)=>{t.styleDeclaration&&t.styleDeclaration(...h),u(...h)},e.canvasMutationCb=(...h)=>{t.canvasMutation&&t.canvasMutation(...h),p(...h)},e.fontCb=(...h)=>{t.font&&t.font(...h),m(...h)},e.selectionCb=(...h)=>{t.selection&&t.selection(...h),f(...h)},e.customElementCb=(...h)=>{t.customElement&&t.customElement(...h),g(...h)}}function nn(e,t={}){const r=e.doc.defaultView;if(!r)return()=>{};rn(e,t);let n;e.recordDOM&&(n=Ft(e,e.doc));const i=jr(e),o=Vr(e),a=Bt(e),l=qr(e,{win:r}),s=Xr(e),c=Qr(e);let u=()=>{},p=()=>{},m=()=>{},f=()=>{};e.recordDOM&&(u=Kr(e,{win:r}),p=Ut(e,e.doc),m=Yr(e,{win:r}),e.collectFonts&&(f=Zr(e)));const g=en(e),h=tn(e),y=[];for(const w of e.plugins)y.push(w.observer(w.callback,r,w.options));return O(()=>{re.forEach(w=>w.reset()),n?.disconnect(),i(),o(),a(),l(),s(),c(),u(),p(),m(),f(),g(),h(),y.forEach(w=>w())})}function Ne(e){return typeof window[e]<"u"}function De(e){return!!(typeof window[e]<"u"&&window[e].prototype&&"insertRule"in window[e].prototype&&"deleteRule"in window[e].prototype)}class Ht{constructor(t){this.generateIdFn=t,this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap}getId(t,r,n,i){const o=n||this.getIdToRemoteIdMap(t),a=i||this.getRemoteIdToIdMap(t);let l=o.get(r);return l||(l=this.generateIdFn(),o.set(r,l),a.set(l,r)),l}getIds(t,r){const n=this.getIdToRemoteIdMap(t),i=this.getRemoteIdToIdMap(t);return r.map(o=>this.getId(t,o,n,i))}getRemoteId(t,r,n){const i=n||this.getRemoteIdToIdMap(t);if(typeof r!="number")return r;const o=i.get(r);return o||-1}getRemoteIds(t,r){const n=this.getRemoteIdToIdMap(t);return r.map(i=>this.getRemoteId(t,i,n))}reset(t){if(!t){this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap;return}this.iframeIdToRemoteIdMap.delete(t),this.iframeRemoteIdToIdMap.delete(t)}getIdToRemoteIdMap(t){let r=this.iframeIdToRemoteIdMap.get(t);return r||(r=new Map,this.iframeIdToRemoteIdMap.set(t,r)),r}getRemoteIdToIdMap(t){let r=this.iframeRemoteIdToIdMap.get(t);return r||(r=new Map,this.iframeRemoteIdToIdMap.set(t,r)),r}}class on{constructor(t){this.iframes=new WeakMap,this.crossOriginIframeMap=new WeakMap,this.crossOriginIframeMirror=new Ht(gt),this.crossOriginIframeRootIdMap=new WeakMap,this.mutationCb=t.mutationCb,this.wrappedEmit=t.wrappedEmit,this.stylesheetManager=t.stylesheetManager,this.recordCrossOriginIframes=t.recordCrossOriginIframes,this.crossOriginIframeStyleMirror=new Ht(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)),this.mirror=t.mirror,this.recordCrossOriginIframes&&window.addEventListener("message",this.handleMessage.bind(this))}addIframe(t){this.iframes.set(t,!0),t.contentWindow&&this.crossOriginIframeMap.set(t.contentWindow,t)}addLoadListener(t){this.loadListener=t}attachIframe(t,r){var n;this.mutationCb({adds:[{parentId:this.mirror.getId(t),nextId:null,node:r}],removes:[],texts:[],attributes:[],isAttachIframe:!0}),(n=this.loadListener)===null||n===void 0||n.call(this,t),t.contentDocument&&t.contentDocument.adoptedStyleSheets&&t.contentDocument.adoptedStyleSheets.length>0&&this.stylesheetManager.adoptStyleSheets(t.contentDocument.adoptedStyleSheets,this.mirror.getId(t.contentDocument))}handleMessage(t){const r=t;if(r.data.type!=="rrweb"||r.origin!==r.data.origin||!t.source)return;const i=this.crossOriginIframeMap.get(t.source);if(!i)return;const o=this.transformCrossOriginEvent(i,r.data.event);o&&this.wrappedEmit(o,r.data.isCheckout)}transformCrossOriginEvent(t,r){var n;switch(r.type){case _.FullSnapshot:{this.crossOriginIframeMirror.reset(t),this.crossOriginIframeStyleMirror.reset(t),this.replaceIdOnNode(r.data.node,t);const i=r.data.node.id;return this.crossOriginIframeRootIdMap.set(t,i),this.patchRootIdOnNode(r.data.node,i),{timestamp:r.timestamp,type:_.IncrementalSnapshot,data:{source:C.Mutation,adds:[{parentId:this.mirror.getId(t),nextId:null,node:r.data.node}],removes:[],texts:[],attributes:[],isAttachIframe:!0}}}case _.Meta:case _.Load:case _.DomContentLoaded:return!1;case _.Plugin:return r;case _.Custom:return this.replaceIds(r.data.payload,t,["id","parentId","previousId","nextId"]),r;case _.IncrementalSnapshot:switch(r.data.source){case C.Mutation:return r.data.adds.forEach(i=>{this.replaceIds(i,t,["parentId","nextId","previousId"]),this.replaceIdOnNode(i.node,t);const o=this.crossOriginIframeRootIdMap.get(t);o&&this.patchRootIdOnNode(i.node,o)}),r.data.removes.forEach(i=>{this.replaceIds(i,t,["parentId","id"])}),r.data.attributes.forEach(i=>{this.replaceIds(i,t,["id"])}),r.data.texts.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.Drag:case C.TouchMove:case C.MouseMove:return r.data.positions.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.ViewportResize:return!1;case C.MediaInteraction:case C.MouseInteraction:case C.Scroll:case C.CanvasMutation:case C.Input:return this.replaceIds(r.data,t,["id"]),r;case C.StyleSheetRule:case C.StyleDeclaration:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleId"]),r;case C.Font:return r;case C.Selection:return r.data.ranges.forEach(i=>{this.replaceIds(i,t,["start","end"])}),r;case C.AdoptedStyleSheet:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleIds"]),(n=r.data.styles)===null||n===void 0||n.forEach(i=>{this.replaceStyleIds(i,t,["styleId"])}),r}}return!1}replace(t,r,n,i){for(const o of i)!Array.isArray(r[o])&&typeof r[o]!="number"||(Array.isArray(r[o])?r[o]=t.getIds(n,r[o]):r[o]=t.getId(n,r[o]));return r}replaceIds(t,r,n){return this.replace(this.crossOriginIframeMirror,t,r,n)}replaceStyleIds(t,r,n){return this.replace(this.crossOriginIframeStyleMirror,t,r,n)}replaceIdOnNode(t,r){this.replaceIds(t,r,["id","rootId"]),"childNodes"in t&&t.childNodes.forEach(n=>{this.replaceIdOnNode(n,r)})}patchRootIdOnNode(t,r){t.type!==A.Document&&!t.rootId&&(t.rootId=r),"childNodes"in t&&t.childNodes.forEach(n=>{this.patchRootIdOnNode(n,r)})}}class sn{constructor(t){this.shadowDoms=new WeakSet,this.restoreHandlers=[],this.mutationCb=t.mutationCb,this.scrollCb=t.scrollCb,this.bypassOptions=t.bypassOptions,this.mirror=t.mirror,this.init()}init(){this.reset(),this.patchAttachShadow(Element,document)}addShadowRoot(t,r){if(!ye(t)||this.shadowDoms.has(t))return;this.shadowDoms.add(t);const n=Ft(Object.assign(Object.assign({},this.bypassOptions),{doc:r,mutationCb:this.mutationCb,mirror:this.mirror,shadowDomManager:this}),t);this.restoreHandlers.push(()=>n.disconnect()),this.restoreHandlers.push(Bt(Object.assign(Object.assign({},this.bypassOptions),{scrollCb:this.scrollCb,doc:t,mirror:this.mirror}))),setTimeout(()=>{t.adoptedStyleSheets&&t.adoptedStyleSheets.length>0&&this.bypassOptions.stylesheetManager.adoptStyleSheets(t.adoptedStyleSheets,this.mirror.getId(t.host)),this.restoreHandlers.push(Ut({mirror:this.mirror,stylesheetManager:this.bypassOptions.stylesheetManager},t))},0)}observeAttachShadow(t){!t.contentWindow||!t.contentDocument||this.patchAttachShadow(t.contentWindow.Element,t.contentDocument)}patchAttachShadow(t,r){const n=this;this.restoreHandlers.push(he(t.prototype,"attachShadow",function(i){return function(o){const a=i.call(this,o);return this.shadowRoot&&Tt(this)&&n.addShadowRoot(this.shadowRoot,r),a}}))}reset(){this.restoreHandlers.forEach(t=>{try{t()}catch{}}),this.restoreHandlers=[],this.shadowDoms=new WeakSet}}/*! ***************************************************************************** -======= -(function(){"use strict";var A;(function(e){e[e.Document=0]="Document",e[e.DocumentType=1]="DocumentType",e[e.Element=2]="Element",e[e.Text=3]="Text",e[e.CDATA=4]="CDATA",e[e.Comment=5]="Comment"})(A||(A={}));function gr(e){return e.nodeType===e.ELEMENT_NODE}function be(e){const t=e?.host;return t?.shadowRoot===e}function we(e){return Object.prototype.toString.call(e)==="[object ShadowRoot]"}function yr(e){return e.includes(" background-clip: text;")&&!e.includes(" -webkit-background-clip: text;")&&(e=e.replace(" background-clip: text;"," -webkit-background-clip: text; background-clip: text;")),e}function vr(e){const{cssText:t}=e;if(t.split('"').length<3)return t;const r=["@import",`url(${JSON.stringify(e.href)})`];return e.layerName===""?r.push("layer"):e.layerName&&r.push(`layer(${e.layerName})`),e.supportsText&&r.push(`supports(${e.supportsText})`),e.media.length&&r.push(e.media.mediaText),r.join(" ")+";"}function Te(e){try{const t=e.rules||e.cssRules;return t?yr(Array.from(t,vt).join("")):null}catch{return null}}function vt(e){let t;if(br(e))try{t=Te(e.styleSheet)||vr(e)}catch{}else if(wr(e)&&e.selectorText.includes(":"))return Sr(e.cssText);return t||e.cssText}function Sr(e){const t=/(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;return e.replace(t,"$1\\$2")}function br(e){return"styleSheet"in e}function wr(e){return"selectorText"in e}class St{constructor(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}getId(t){var r;if(!t)return-1;const n=(r=this.getMeta(t))===null||r===void 0?void 0:r.id;return n??-1}getNode(t){return this.idNodeMap.get(t)||null}getIds(){return Array.from(this.idNodeMap.keys())}getMeta(t){return this.nodeMetaMap.get(t)||null}removeNodeFromMap(t){const r=this.getId(t);this.idNodeMap.delete(r),t.childNodes&&t.childNodes.forEach(n=>this.removeNodeFromMap(n))}has(t){return this.idNodeMap.has(t)}hasNode(t){return this.nodeMetaMap.has(t)}add(t,r){const n=r.id;this.idNodeMap.set(n,t),this.nodeMetaMap.set(t,r)}replace(t,r){const n=this.getNode(t);if(n){const i=this.nodeMetaMap.get(n);i&&this.nodeMetaMap.set(r,i)}this.idNodeMap.set(t,r)}reset(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}}function Ir(){return new St}function et({element:e,maskInputOptions:t,tagName:r,type:n,value:i,maskInputFn:o}){let a=i||"";const l=n&&ie(n);return(t[r.toLowerCase()]||l&&t[l])&&(o?a=o(a,e):a="*".repeat(a.length)),a}function ie(e){return e.toLowerCase()}const bt="__rrweb_original__";function Mr(e){const t=e.getContext("2d");if(!t)return!0;const r=50;for(let n=0;ns!==0))return!1}return!0}function tt(e){const t=e.type;return e.hasAttribute("data-rr-is-password")?"password":t?ie(t):null}function wt(e,t){var r;let n;try{n=new URL(e,t??window.location.href)}catch{return null}const i=/\.([0-9a-z]+)(?:$)/i,o=n.pathname.match(i);return(r=o?.[1])!==null&&r!==void 0?r:null}let Cr=1;const _r=new RegExp("[^a-z0-9-_:]"),Ie=-2;function It(){return Cr++}function Er(e){if(e instanceof HTMLFormElement)return"form";const t=ie(e.tagName);return _r.test(t)?"div":t}function Or(e){let t="";return e.indexOf("//")>-1?t=e.split("/").slice(0,3).join("/"):t=e.split("/")[0],t=t.split("?")[0],t}let fe,Mt;const xr=/url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm,kr=/^(?:[a-z+]+:)?\/\//i,Tr=/^www\..*/i,Rr=/^(data:)([^,]*),(.*)/i;function Re(e,t){return(e||"").replace(xr,(r,n,i,o,a,l)=>{const s=i||a||l,u=n||o||"";if(!s)return r;if(kr.test(s)||Tr.test(s))return`url(${u}${s}${u})`;if(Rr.test(s))return`url(${u}${s}${u})`;if(s[0]==="/")return`url(${u}${Or(t)+s}${u})`;const c=t.split("/"),h=s.split("/");c.pop();for(const m of h)m!=="."&&(m===".."?c.pop():c.push(m));return`url(${u}${c.join("/")}${u})`})}const Dr=/^[^ \t\n\r\u000c]+/,Nr=/^[, \t\n\r\u000c]+/;function Ar(e,t){if(t.trim()==="")return t;let r=0;function n(o){let a;const l=o.exec(t.substring(r));return l?(a=l[0],r+=a.length,a):""}const i=[];for(;n(Nr),!(r>=t.length);){let o=n(Dr);if(o.slice(-1)===",")o=he(e,o.substring(0,o.length-1)),i.push(o);else{let a="";o=he(e,o);let l=!1;for(;;){const s=t.charAt(r);if(s===""){i.push((o+a).trim());break}else if(l)s===")"&&(l=!1);else if(s===","){r+=1,i.push((o+a).trim());break}else s==="("&&(l=!0);a+=s,r+=1}}}return i.join(", ")}function he(e,t){if(!t||t.trim()==="")return t;const r=e.createElement("a");return r.href=t,r.href}function Lr(e){return!!(e.tagName==="svg"||e.ownerSVGElement)}function rt(){const e=document.createElement("a");return e.href="",e.href}function Ct(e,t,r,n){return n&&(r==="src"||r==="href"&&!(t==="use"&&n[0]==="#")||r==="xlink:href"&&n[0]!=="#"||r==="background"&&(t==="table"||t==="td"||t==="th")?he(e,n):r==="srcset"?Ar(e,n):r==="style"?Re(n,rt()):t==="object"&&r==="data"?he(e,n):n)}function _t(e,t,r){return(e==="video"||e==="audio")&&t==="autoplay"}function Fr(e,t,r){try{if(typeof t=="string"){if(e.classList.contains(t))return!0}else for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}if(r)return e.matches(r)}catch{}return!1}function De(e,t,r){if(!e)return!1;if(e.nodeType!==e.ELEMENT_NODE)return r?De(e.parentNode,t,r):!1;for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}return r?De(e.parentNode,t,r):!1}function Et(e,t,r,n){try{const i=e.nodeType===e.ELEMENT_NODE?e:e.parentElement;if(i===null)return!1;if(typeof t=="string"){if(n){if(i.closest(`.${t}`))return!0}else if(i.classList.contains(t))return!0}else if(De(i,t,n))return!0;if(r){if(n){if(i.closest(r))return!0}else if(i.matches(r))return!0}}catch{}return!1}function Pr(e,t,r){const n=e.contentWindow;if(!n)return;let i=!1,o;try{o=n.document.readyState}catch{return}if(o!=="complete"){const l=setTimeout(()=>{i||(t(),i=!0)},r);e.addEventListener("load",()=>{clearTimeout(l),i=!0,t()});return}const a="about:blank";if(n.location.href!==a||e.src===a||e.src==="")return setTimeout(t,0),e.addEventListener("load",t);e.addEventListener("load",t)}function Br(e,t,r){let n=!1,i;try{i=e.sheet}catch{return}if(i)return;const o=setTimeout(()=>{n||(t(),n=!0)},r);e.addEventListener("load",()=>{clearTimeout(o),n=!0,t()})}function Wr(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:a,inlineStylesheet:l,maskInputOptions:s={},maskTextFn:u,maskInputFn:c,dataURLOptions:h={},inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:p=!1}=t,y=Ur(r,n);switch(e.nodeType){case e.DOCUMENT_NODE:return e.compatMode!=="CSS1Compat"?{type:A.Document,childNodes:[],compatMode:e.compatMode}:{type:A.Document,childNodes:[]};case e.DOCUMENT_TYPE_NODE:return{type:A.DocumentType,name:e.name,publicId:e.publicId,systemId:e.systemId,rootId:y};case e.ELEMENT_NODE:return zr(e,{doc:r,blockClass:i,blockSelector:o,inlineStylesheet:l,maskInputOptions:s,maskInputFn:c,dataURLOptions:h,inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:p,rootId:y});case e.TEXT_NODE:return Hr(e,{needsMask:a,maskTextFn:u,rootId:y});case e.CDATA_SECTION_NODE:return{type:A.CDATA,textContent:"",rootId:y};case e.COMMENT_NODE:return{type:A.Comment,textContent:e.textContent||"",rootId:y};default:return!1}}function Ur(e,t){if(!t.hasNode(e))return;const r=t.getId(e);return r===1?void 0:r}function Hr(e,t){var r;const{needsMask:n,maskTextFn:i,rootId:o}=t,a=e.parentNode&&e.parentNode.tagName;let l=e.textContent;const s=a==="STYLE"?!0:void 0,u=a==="SCRIPT"?!0:void 0;if(s&&l){try{e.nextSibling||e.previousSibling||!((r=e.parentNode.sheet)===null||r===void 0)&&r.cssRules&&(l=Te(e.parentNode.sheet))}catch(c){console.warn(`Cannot get CSS styles from text's parentNode. Error: ${c}`,e)}l=Re(l,rt())}return u&&(l="SCRIPT_PLACEHOLDER"),!s&&!u&&l&&n&&(l=i?i(l,e.parentElement):l.replace(/[\S]/g,"*")),{type:A.Text,textContent:l||"",isStyle:s,rootId:o}}function zr(e,t){const{doc:r,blockClass:n,blockSelector:i,inlineStylesheet:o,maskInputOptions:a={},maskInputFn:l,dataURLOptions:s={},inlineImages:u,recordCanvas:c,keepIframeSrcFn:h,newlyAddedElement:m=!1,rootId:f}=t,g=Fr(e,n,i),p=Er(e);let y={};const w=e.attributes.length;for(let v=0;vI.href===e.href);let b=null;v&&(b=Te(v)),b&&(delete y.rel,delete y.href,y._cssText=Re(b,v.href))}if(p==="style"&&e.sheet&&!(e.innerText||e.textContent||"").trim().length){const v=Te(e.sheet);v&&(y._cssText=Re(v,rt()))}if(p==="input"||p==="textarea"||p==="select"){const v=e.value,b=e.checked;y.type!=="radio"&&y.type!=="checkbox"&&y.type!=="submit"&&y.type!=="button"&&v?y.value=et({element:e,type:tt(e),tagName:p,value:v,maskInputOptions:a,maskInputFn:l}):b&&(y.checked=b)}if(p==="option"&&(e.selected&&!a.select?y.selected=!0:delete y.selected),p==="canvas"&&c){if(e.__context==="2d")Mr(e)||(y.rr_dataURL=e.toDataURL(s.type,s.quality));else if(!("__context"in e)){const v=e.toDataURL(s.type,s.quality),b=document.createElement("canvas");b.width=e.width,b.height=e.height;const I=b.toDataURL(s.type,s.quality);v!==I&&(y.rr_dataURL=v)}}if(p==="img"&&u){fe||(fe=r.createElement("canvas"),Mt=fe.getContext("2d"));const v=e,b=v.crossOrigin;v.crossOrigin="anonymous";const I=()=>{v.removeEventListener("load",I);try{fe.width=v.naturalWidth,fe.height=v.naturalHeight,Mt.drawImage(v,0,0),y.rr_dataURL=fe.toDataURL(s.type,s.quality)}catch(B){console.warn(`Cannot inline img src=${v.currentSrc}! Error: ${B}`)}b?y.crossOrigin=b:v.removeAttribute("crossorigin")};v.complete&&v.naturalWidth!==0?I():v.addEventListener("load",I)}if(p==="audio"||p==="video"){const v=y;v.rr_mediaState=e.paused?"paused":"played",v.rr_mediaCurrentTime=e.currentTime,v.rr_mediaPlaybackRate=e.playbackRate,v.rr_mediaMuted=e.muted,v.rr_mediaLoop=e.loop,v.rr_mediaVolume=e.volume}if(m||(e.scrollLeft&&(y.rr_scrollLeft=e.scrollLeft),e.scrollTop&&(y.rr_scrollTop=e.scrollTop)),g){const{width:v,height:b}=e.getBoundingClientRect();y={class:y.class,rr_width:`${v}px`,rr_height:`${b}px`}}p==="iframe"&&!h(y.src)&&(e.contentDocument||(y.rr_src=y.src),delete y.src);let S;try{customElements.get(p)&&(S=!0)}catch{}return{type:A.Element,tagName:p,attributes:y,childNodes:[],isSVG:Lr(e)||void 0,needBlock:g,rootId:f,isCustom:S}}function x(e){return e==null?"":e.toLowerCase()}function qr(e,t){if(t.comment&&e.type===A.Comment)return!0;if(e.type===A.Element){if(t.script&&(e.tagName==="script"||e.tagName==="link"&&(e.attributes.rel==="preload"||e.attributes.rel==="modulepreload")&&e.attributes.as==="script"||e.tagName==="link"&&e.attributes.rel==="prefetch"&&typeof e.attributes.href=="string"&&wt(e.attributes.href)==="js"))return!0;if(t.headFavicon&&(e.tagName==="link"&&e.attributes.rel==="shortcut icon"||e.tagName==="meta"&&(x(e.attributes.name).match(/^msapplication-tile(image|color)$/)||x(e.attributes.name)==="application-name"||x(e.attributes.rel)==="icon"||x(e.attributes.rel)==="apple-touch-icon"||x(e.attributes.rel)==="shortcut icon")))return!0;if(e.tagName==="meta"){if(t.headMetaDescKeywords&&x(e.attributes.name).match(/^description|keywords$/))return!0;if(t.headMetaSocial&&(x(e.attributes.property).match(/^(og|twitter|fb):/)||x(e.attributes.name).match(/^(og|twitter):/)||x(e.attributes.name)==="pinterest"))return!0;if(t.headMetaRobots&&(x(e.attributes.name)==="robots"||x(e.attributes.name)==="googlebot"||x(e.attributes.name)==="bingbot"))return!0;if(t.headMetaHttpEquiv&&e.attributes["http-equiv"]!==void 0)return!0;if(t.headMetaAuthorship&&(x(e.attributes.name)==="author"||x(e.attributes.name)==="generator"||x(e.attributes.name)==="framework"||x(e.attributes.name)==="publisher"||x(e.attributes.name)==="progid"||x(e.attributes.property).match(/^article:/)||x(e.attributes.property).match(/^product:/)))return!0;if(t.headMetaVerification&&(x(e.attributes.name)==="google-site-verification"||x(e.attributes.name)==="yandex-verification"||x(e.attributes.name)==="csrf-token"||x(e.attributes.name)==="p:domain_verify"||x(e.attributes.name)==="verify-v1"||x(e.attributes.name)==="verification"||x(e.attributes.name)==="shopify-checkout-api-token"))return!0}}return!1}function pe(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,maskTextClass:a,maskTextSelector:l,skipChild:s=!1,inlineStylesheet:u=!0,maskInputOptions:c={},maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g={},inlineImages:p=!1,recordCanvas:y=!1,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v=5e3,onStylesheetLoad:b,stylesheetLoadTimeout:I=5e3,keepIframeSrcFn:B=()=>!1,newlyAddedElement:P=!1}=t;let{needsMask:k}=t,{preserveWhiteSpace:T=!0}=t;!k&&e.childNodes&&(k=Et(e,a,l,k===void 0));const G=Wr(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,dataURLOptions:g,inlineImages:p,recordCanvas:y,keepIframeSrcFn:B,newlyAddedElement:P});if(!G)return console.warn(e,"not serialized"),null;let V;n.hasNode(e)?V=n.getId(e):qr(G,f)||!T&&G.type===A.Text&&!G.isStyle&&!G.textContent.replace(/^\s+|\s+$/gm,"").length?V=Ie:V=It();const O=Object.assign(G,{id:V});if(n.add(e,O),V===Ie)return null;w&&w(e);let le=!s;if(O.type===A.Element){le=le&&!O.needBlock,delete O.needBlock;const z=e.shadowRoot;z&&we(z)&&(O.isShadowHost=!0)}if((O.type===A.Document||O.type===A.Element)&&le){f.headWhitespace&&O.type===A.Element&&O.tagName==="head"&&(T=!1);const z={doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:s,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B};if(!(O.type===A.Element&&O.tagName==="textarea"&&O.attributes.value!==void 0))for(const ne of Array.from(e.childNodes)){const Z=pe(ne,z);Z&&O.childNodes.push(Z)}if(gr(e)&&e.shadowRoot)for(const ne of Array.from(e.shadowRoot.childNodes)){const Z=pe(ne,z);Z&&(we(e.shadowRoot)&&(Z.isShadow=!0),O.childNodes.push(Z))}}return e.parentNode&&be(e.parentNode)&&we(e.parentNode)&&(O.isShadow=!0),O.type===A.Element&&O.tagName==="iframe"&&Pr(e,()=>{const z=e.contentDocument;if(z&&S){const ne=pe(z,{doc:z,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B});ne&&S(e,ne)}},v),O.type===A.Element&&O.tagName==="link"&&typeof O.attributes.rel=="string"&&(O.attributes.rel==="stylesheet"||O.attributes.rel==="preload"&&typeof O.attributes.href=="string"&&wt(O.attributes.href)==="css")&&Br(e,()=>{if(b){const z=pe(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:u,maskInputOptions:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B});z&&b(e,z)}},I),O}function $r(e,t){const{mirror:r=new St,blockClass:n="rr-block",blockSelector:i=null,maskTextClass:o="rr-mask",maskTextSelector:a=null,inlineStylesheet:l=!0,inlineImages:s=!1,recordCanvas:u=!1,maskAllInputs:c=!1,maskTextFn:h,maskInputFn:m,slimDOM:f=!1,dataURLOptions:g,preserveWhiteSpace:p,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:S,onStylesheetLoad:v,stylesheetLoadTimeout:b,keepIframeSrcFn:I=()=>!1}=t||{};return pe(e,{doc:e,mirror:r,blockClass:n,blockSelector:i,maskTextClass:o,maskTextSelector:a,skipChild:!1,inlineStylesheet:l,maskInputOptions:c===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:c===!1?{password:!0}:c,maskTextFn:h,maskInputFn:m,slimDOMOptions:f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:f==="all",headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0}:f===!1?{}:f,dataURLOptions:g,inlineImages:s,recordCanvas:u,preserveWhiteSpace:p,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:S,onStylesheetLoad:v,stylesheetLoadTimeout:b,keepIframeSrcFn:I,newlyAddedElement:!1})}function U(e,t,r=document){const n={capture:!0,passive:!0};return r.addEventListener(e,t,n),()=>r.removeEventListener(e,t,n)}const me=`Please stop import mirror directly. Instead of that,\r -now you can use replayer.getMirror() to access the mirror instance of a replayer,\r -or you can use record.mirror to access the mirror instance during recording.`;let Ot={map:{},getId(){return console.error(me),-1},getNode(){return console.error(me),null},removeNodeFromMap(){console.error(me)},has(){return console.error(me),!1},reset(){console.error(me)}};typeof window<"u"&&window.Proxy&&window.Reflect&&(Ot=new Proxy(Ot,{get(e,t,r){return t==="map"&&console.error(me),Reflect.get(e,t,r)}}));function Me(e,t,r={}){let n=null,i=0;return function(...o){const a=Date.now();!i&&r.leading===!1&&(i=a);const l=t-(a-i),s=this;l<=0||l>t?(n&&(clearTimeout(n),n=null),i=a,e.apply(s,o)):!n&&r.trailing!==!1&&(n=setTimeout(()=>{i=r.leading===!1?0:Date.now(),n=null,e.apply(s,o)},l))}}function Ne(e,t,r,n,i=window){const o=i.Object.getOwnPropertyDescriptor(e,t);return i.Object.defineProperty(e,t,n?r:{set(a){setTimeout(()=>{r.set.call(this,a)},0),o&&o.set&&o.set.call(this,a)}}),()=>Ne(e,t,o||{},!0)}function ge(e,t,r){try{if(!(t in e))return()=>{};const n=e[t],i=r(n);return typeof i=="function"&&(i.prototype=i.prototype||{},Object.defineProperties(i,{__rrweb_original__:{enumerable:!1,value:n}})),e[t]=i,()=>{e[t]=n}}catch{return()=>{}}}let Ae=Date.now;/[1-9][0-9]{12}/.test(Date.now().toString())||(Ae=()=>new Date().getTime());function xt(e){var t,r,n,i,o,a;const l=e.document;return{left:l.scrollingElement?l.scrollingElement.scrollLeft:e.pageXOffset!==void 0?e.pageXOffset:l?.documentElement.scrollLeft||((r=(t=l?.body)===null||t===void 0?void 0:t.parentElement)===null||r===void 0?void 0:r.scrollLeft)||((n=l?.body)===null||n===void 0?void 0:n.scrollLeft)||0,top:l.scrollingElement?l.scrollingElement.scrollTop:e.pageYOffset!==void 0?e.pageYOffset:l?.documentElement.scrollTop||((o=(i=l?.body)===null||i===void 0?void 0:i.parentElement)===null||o===void 0?void 0:o.scrollTop)||((a=l?.body)===null||a===void 0?void 0:a.scrollTop)||0}}function kt(){return window.innerHeight||document.documentElement&&document.documentElement.clientHeight||document.body&&document.body.clientHeight}function Tt(){return window.innerWidth||document.documentElement&&document.documentElement.clientWidth||document.body&&document.body.clientWidth}function Rt(e){return e?e.nodeType===e.ELEMENT_NODE?e:e.parentElement:null}function H(e,t,r,n){if(!e)return!1;const i=Rt(e);if(!i)return!1;try{if(typeof t=="string"){if(i.classList.contains(t)||n&&i.closest("."+t)!==null)return!0}else if(De(i,t,n))return!0}catch{}return!!(r&&(i.matches(r)||n&&i.closest(r)!==null))}function jr(e,t){return t.getId(e)!==-1}function nt(e,t){return t.getId(e)===Ie}function Dt(e,t){if(be(e))return!1;const r=t.getId(e);return t.has(r)?e.parentNode&&e.parentNode.nodeType===e.DOCUMENT_NODE?!1:e.parentNode?Dt(e.parentNode,t):!0:!0}function it(e){return!!e.changedTouches}function Gr(e=window){"NodeList"in e&&!e.NodeList.prototype.forEach&&(e.NodeList.prototype.forEach=Array.prototype.forEach),"DOMTokenList"in e&&!e.DOMTokenList.prototype.forEach&&(e.DOMTokenList.prototype.forEach=Array.prototype.forEach),Node.prototype.contains||(Node.prototype.contains=(...t)=>{let r=t[0];if(!(0 in t))throw new TypeError("1 argument is required");do if(this===r)return!0;while(r=r&&r.parentNode);return!1})}function Nt(e,t){return!!(e.nodeName==="IFRAME"&&t.getMeta(e))}function At(e,t){return!!(e.nodeName==="LINK"&&e.nodeType===e.ELEMENT_NODE&&e.getAttribute&&e.getAttribute("rel")==="stylesheet"&&t.getMeta(e))}function ot(e){return!!e?.shadowRoot}class Vr{constructor(){this.id=1,this.styleIDMap=new WeakMap,this.idStyleMap=new Map}getId(t){var r;return(r=this.styleIDMap.get(t))!==null&&r!==void 0?r:-1}has(t){return this.styleIDMap.has(t)}add(t,r){if(this.has(t))return this.getId(t);let n;return r===void 0?n=this.id++:n=r,this.styleIDMap.set(t,n),this.idStyleMap.set(n,t),n}getStyle(t){return this.idStyleMap.get(t)||null}reset(){this.styleIDMap=new WeakMap,this.idStyleMap=new Map,this.id=1}generateId(){return this.id++}}function Lt(e){var t,r;let n=null;return((r=(t=e.getRootNode)===null||t===void 0?void 0:t.call(e))===null||r===void 0?void 0:r.nodeType)===Node.DOCUMENT_FRAGMENT_NODE&&e.getRootNode().host&&(n=e.getRootNode().host),n}function Jr(e){let t=e,r;for(;r=Lt(t);)t=r;return t}function Xr(e){const t=e.ownerDocument;if(!t)return!1;const r=Jr(e);return t.contains(r)}function Ft(e){const t=e.ownerDocument;return t?t.contains(e)||Xr(e):!1}var E=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(E||{}),C=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(C||{}),$=(e=>(e[e.MouseUp=0]="MouseUp",e[e.MouseDown=1]="MouseDown",e[e.Click=2]="Click",e[e.ContextMenu=3]="ContextMenu",e[e.DblClick=4]="DblClick",e[e.Focus=5]="Focus",e[e.Blur=6]="Blur",e[e.TouchStart=7]="TouchStart",e[e.TouchMove_Departed=8]="TouchMove_Departed",e[e.TouchEnd=9]="TouchEnd",e[e.TouchCancel=10]="TouchCancel",e))($||{}),ee=(e=>(e[e.Mouse=0]="Mouse",e[e.Pen=1]="Pen",e[e.Touch=2]="Touch",e))(ee||{}),ye=(e=>(e[e["2D"]=0]="2D",e[e.WebGL=1]="WebGL",e[e.WebGL2=2]="WebGL2",e))(ye||{});function Pt(e){return"__ln"in e}class Kr{constructor(){this.length=0,this.head=null,this.tail=null}get(t){if(t>=this.length)throw new Error("Position outside of list range");let r=this.head;for(let n=0;n`${e}@${t}`;class Yr{constructor(){this.frozen=!1,this.locked=!1,this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.mapRemoves=[],this.movedMap={},this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.processMutations=t=>{t.forEach(this.processMutation),this.emit()},this.emit=()=>{if(this.frozen||this.locked)return;const t=[],r=new Set,n=new Kr,i=s=>{let u=s,c=Ie;for(;c===Ie;)u=u&&u.nextSibling,c=u&&this.mirror.getId(u);return c},o=s=>{if(!s.parentNode||!Ft(s)||s.parentNode.tagName==="TEXTAREA")return;const u=be(s.parentNode)?this.mirror.getId(Lt(s)):this.mirror.getId(s.parentNode),c=i(s);if(u===-1||c===-1)return n.addNode(s);const h=pe(s,{doc:this.doc,mirror:this.mirror,blockClass:this.blockClass,blockSelector:this.blockSelector,maskTextClass:this.maskTextClass,maskTextSelector:this.maskTextSelector,skipChild:!0,newlyAddedElement:!0,inlineStylesheet:this.inlineStylesheet,maskInputOptions:this.maskInputOptions,maskTextFn:this.maskTextFn,maskInputFn:this.maskInputFn,slimDOMOptions:this.slimDOMOptions,dataURLOptions:this.dataURLOptions,recordCanvas:this.recordCanvas,inlineImages:this.inlineImages,onSerialize:m=>{Nt(m,this.mirror)&&this.iframeManager.addIframe(m),At(m,this.mirror)&&this.stylesheetManager.trackLinkElement(m),ot(s)&&this.shadowDomManager.addShadowRoot(s.shadowRoot,this.doc)},onIframeLoad:(m,f)=>{this.iframeManager.attachIframe(m,f),this.shadowDomManager.observeAttachShadow(m)},onStylesheetLoad:(m,f)=>{this.stylesheetManager.attachLinkElement(m,f)}});h&&(t.push({parentId:u,nextId:c,node:h}),r.add(h.id))};for(;this.mapRemoves.length;)this.mirror.removeNodeFromMap(this.mapRemoves.shift());for(const s of this.movedSet)Wt(this.removes,s,this.mirror)&&!this.movedSet.has(s.parentNode)||o(s);for(const s of this.addedSet)!Ht(this.droppedSet,s)&&!Wt(this.removes,s,this.mirror)||Ht(this.movedSet,s)?o(s):this.droppedSet.add(s);let a=null;for(;n.length;){let s=null;if(a){const u=this.mirror.getId(a.value.parentNode),c=i(a.value);u!==-1&&c!==-1&&(s=a)}if(!s){let u=n.tail;for(;u;){const c=u;if(u=u.previous,c){const h=this.mirror.getId(c.value.parentNode);if(i(c.value)===-1)continue;if(h!==-1){s=c;break}else{const f=c.value;if(f.parentNode&&f.parentNode.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const g=f.parentNode.host;if(this.mirror.getId(g)!==-1){s=c;break}}}}}}if(!s){for(;n.head;)n.removeNode(n.head.value);break}a=s.previous,n.removeNode(s.value),o(s.value)}const l={texts:this.texts.map(s=>{const u=s.node;return u.parentNode&&u.parentNode.tagName==="TEXTAREA"&&this.genTextAreaValueMutation(u.parentNode),{id:this.mirror.getId(u),value:s.value}}).filter(s=>!r.has(s.id)).filter(s=>this.mirror.has(s.id)),attributes:this.attributes.map(s=>{const{attributes:u}=s;if(typeof u.style=="string"){const c=JSON.stringify(s.styleDiff),h=JSON.stringify(s._unchangedStyles);c.length!r.has(s.id)).filter(s=>this.mirror.has(s.id)),removes:this.removes,adds:t};!l.texts.length&&!l.attributes.length&&!l.removes.length&&!l.adds.length||(this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.movedMap={},this.mutationCb(l))},this.genTextAreaValueMutation=t=>{let r=this.attributeMap.get(t);r||(r={node:t,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(r),this.attributeMap.set(t,r)),r.attributes.value=Array.from(t.childNodes,n=>n.textContent||"").join("")},this.processMutation=t=>{if(!nt(t.target,this.mirror))switch(t.type){case"characterData":{const r=t.target.textContent;!H(t.target,this.blockClass,this.blockSelector,!1)&&r!==t.oldValue&&this.texts.push({value:Et(t.target,this.maskTextClass,this.maskTextSelector,!0)&&r?this.maskTextFn?this.maskTextFn(r,Rt(t.target)):r.replace(/[\S]/g,"*"):r,node:t.target});break}case"attributes":{const r=t.target;let n=t.attributeName,i=t.target.getAttribute(n);if(n==="value"){const a=tt(r);i=et({element:r,maskInputOptions:this.maskInputOptions,tagName:r.tagName,type:a,value:i,maskInputFn:this.maskInputFn})}if(H(t.target,this.blockClass,this.blockSelector,!1)||i===t.oldValue)return;let o=this.attributeMap.get(t.target);if(r.tagName==="IFRAME"&&n==="src"&&!this.keepIframeSrcFn(i))if(!r.contentDocument)n="rr_src";else return;if(o||(o={node:t.target,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(o),this.attributeMap.set(t.target,o)),n==="type"&&r.tagName==="INPUT"&&(t.oldValue||"").toLowerCase()==="password"&&r.setAttribute("data-rr-is-password","true"),!_t(r.tagName,n)&&(o.attributes[n]=Ct(this.doc,ie(r.tagName),ie(n),i),n==="style")){if(!this.unattachedDoc)try{this.unattachedDoc=document.implementation.createHTMLDocument()}catch{this.unattachedDoc=this.doc}const a=this.unattachedDoc.createElement("span");t.oldValue&&a.setAttribute("style",t.oldValue);for(const l of Array.from(r.style)){const s=r.style.getPropertyValue(l),u=r.style.getPropertyPriority(l);s!==a.style.getPropertyValue(l)||u!==a.style.getPropertyPriority(l)?u===""?o.styleDiff[l]=s:o.styleDiff[l]=[s,u]:o._unchangedStyles[l]=[s,u]}for(const l of Array.from(a.style))r.style.getPropertyValue(l)===""&&(o.styleDiff[l]=!1)}break}case"childList":{if(H(t.target,this.blockClass,this.blockSelector,!0))return;if(t.target.tagName==="TEXTAREA"){this.genTextAreaValueMutation(t.target);return}t.addedNodes.forEach(r=>this.genAdds(r,t.target)),t.removedNodes.forEach(r=>{const n=this.mirror.getId(r),i=be(t.target)?this.mirror.getId(t.target.host):this.mirror.getId(t.target);H(t.target,this.blockClass,this.blockSelector,!1)||nt(r,this.mirror)||!jr(r,this.mirror)||(this.addedSet.has(r)?(st(this.addedSet,r),this.droppedSet.add(r)):this.addedSet.has(t.target)&&n===-1||Dt(t.target,this.mirror)||(this.movedSet.has(r)&&this.movedMap[Bt(n,i)]?st(this.movedSet,r):this.removes.push({parentId:i,id:n,isShadow:be(t.target)&&we(t.target)?!0:void 0})),this.mapRemoves.push(r))});break}}},this.genAdds=(t,r)=>{if(!this.processedNodeManager.inOtherBuffer(t,this)&&!(this.addedSet.has(t)||this.movedSet.has(t))){if(this.mirror.hasNode(t)){if(nt(t,this.mirror))return;this.movedSet.add(t);let n=null;r&&this.mirror.hasNode(r)&&(n=this.mirror.getId(r)),n&&n!==-1&&(this.movedMap[Bt(this.mirror.getId(t),n)]=!0)}else this.addedSet.add(t),this.droppedSet.delete(t);H(t,this.blockClass,this.blockSelector,!1)||(t.childNodes.forEach(n=>this.genAdds(n)),ot(t)&&t.shadowRoot.childNodes.forEach(n=>{this.processedNodeManager.add(n,this),this.genAdds(n,t)}))}}}init(t){["mutationCb","blockClass","blockSelector","maskTextClass","maskTextSelector","inlineStylesheet","maskInputOptions","maskTextFn","maskInputFn","keepIframeSrcFn","recordCanvas","inlineImages","slimDOMOptions","dataURLOptions","doc","mirror","iframeManager","stylesheetManager","shadowDomManager","canvasManager","processedNodeManager"].forEach(r=>{this[r]=t[r]})}freeze(){this.frozen=!0,this.canvasManager.freeze()}unfreeze(){this.frozen=!1,this.canvasManager.unfreeze(),this.emit()}isFrozen(){return this.frozen}lock(){this.locked=!0,this.canvasManager.lock()}unlock(){this.locked=!1,this.canvasManager.unlock(),this.emit()}reset(){this.shadowDomManager.reset(),this.canvasManager.reset()}}function st(e,t){e.delete(t),t.childNodes.forEach(r=>st(e,r))}function Wt(e,t,r){return e.length===0?!1:Ut(e,t,r)}function Ut(e,t,r){const{parentNode:n}=t;if(!n)return!1;const i=r.getId(n);return e.some(o=>o.id===i)?!0:Ut(e,n,r)}function Ht(e,t){return e.size===0?!1:zt(e,t)}function zt(e,t){const{parentNode:r}=t;return r?e.has(r)?!0:zt(e,r):!1}let Ce;function Qr(e){Ce=e}function Zr(){Ce=void 0}const _=e=>Ce?(...r)=>{try{return e(...r)}catch(n){if(Ce&&Ce(n)===!0)return;throw n}}:e,oe=[];function _e(e){try{if("composedPath"in e){const t=e.composedPath();if(t.length)return t[0]}else if("path"in e&&e.path.length)return e.path[0]}catch{}return e&&e.target}function qt(e,t){var r,n;const i=new Yr;oe.push(i),i.init(e);let o=window.MutationObserver||window.__rrMutationObserver;const a=(n=(r=window?.Zone)===null||r===void 0?void 0:r.__symbol__)===null||n===void 0?void 0:n.call(r,"MutationObserver");a&&window[a]&&(o=window[a]);const l=new o(_(i.processMutations.bind(i)));return l.observe(t,{attributes:!0,attributeOldValue:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0}),l}function en({mousemoveCb:e,sampling:t,doc:r,mirror:n}){if(t.mousemove===!1)return()=>{};const i=typeof t.mousemove=="number"?t.mousemove:50,o=typeof t.mousemoveCallback=="number"?t.mousemoveCallback:500;let a=[],l;const s=Me(_(h=>{const m=Date.now()-l;e(a.map(f=>(f.timeOffset-=m,f)),h),a=[],l=null}),o),u=_(Me(_(h=>{const m=_e(h),{clientX:f,clientY:g}=it(h)?h.changedTouches[0]:h;l||(l=Ae()),a.push({x:f,y:g,id:n.getId(m),timeOffset:Ae()-l}),s(typeof DragEvent<"u"&&h instanceof DragEvent?C.Drag:h instanceof MouseEvent?C.MouseMove:C.TouchMove)}),i,{trailing:!1})),c=[U("mousemove",u,r),U("touchmove",u,r),U("drag",u,r)];return _(()=>{c.forEach(h=>h())})}function tn({mouseInteractionCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){if(o.mouseInteraction===!1)return()=>{};const a=o.mouseInteraction===!0||o.mouseInteraction===void 0?{}:o.mouseInteraction,l=[];let s=null;const u=c=>h=>{const m=_e(h);if(H(m,n,i,!0))return;let f=null,g=c;if("pointerType"in h){switch(h.pointerType){case"mouse":f=ee.Mouse;break;case"touch":f=ee.Touch;break;case"pen":f=ee.Pen;break}f===ee.Touch?$[c]===$.MouseDown?g="TouchStart":$[c]===$.MouseUp&&(g="TouchEnd"):ee.Pen}else it(h)&&(f=ee.Touch);f!==null?(s=f,(g.startsWith("Touch")&&f===ee.Touch||g.startsWith("Mouse")&&f===ee.Mouse)&&(f=null)):$[c]===$.Click&&(f=s,s=null);const p=it(h)?h.changedTouches[0]:h;if(!p)return;const y=r.getId(m),{clientX:w,clientY:S}=p;_(e)(Object.assign({type:$[g],id:y,x:w,y:S},f!==null&&{pointerType:f}))};return Object.keys($).filter(c=>Number.isNaN(Number(c))&&!c.endsWith("_Departed")&&a[c]!==!1).forEach(c=>{let h=ie(c);const m=u(c);if(window.PointerEvent)switch($[c]){case $.MouseDown:case $.MouseUp:h=h.replace("mouse","pointer");break;case $.TouchStart:case $.TouchEnd:return}l.push(U(h,m,t))}),_(()=>{l.forEach(c=>c())})}function $t({scrollCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){const a=_(Me(_(l=>{const s=_e(l);if(!s||H(s,n,i,!0))return;const u=r.getId(s);if(s===t&&t.defaultView){const c=xt(t.defaultView);e({id:u,x:c.left,y:c.top})}else e({id:u,x:s.scrollLeft,y:s.scrollTop})}),o.scroll||100));return U("scroll",a,t)}function rn({viewportResizeCb:e},{win:t}){let r=-1,n=-1;const i=_(Me(_(()=>{const o=kt(),a=Tt();(r!==o||n!==a)&&(e({width:Number(a),height:Number(o)}),r=o,n=a)}),200));return U("resize",i,t)}const nn=["INPUT","TEXTAREA","SELECT"],jt=new WeakMap;function on({inputCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,ignoreClass:o,ignoreSelector:a,maskInputOptions:l,maskInputFn:s,sampling:u,userTriggeredOnInput:c}){function h(S){let v=_e(S);const b=S.isTrusted,I=v&&v.tagName;if(v&&I==="OPTION"&&(v=v.parentElement),!v||!I||nn.indexOf(I)<0||H(v,n,i,!0)||v.classList.contains(o)||a&&v.matches(a))return;let B=v.value,P=!1;const k=tt(v)||"";k==="radio"||k==="checkbox"?P=v.checked:(l[I.toLowerCase()]||l[k])&&(B=et({element:v,maskInputOptions:l,tagName:I,type:k,value:B,maskInputFn:s})),m(v,c?{text:B,isChecked:P,userTriggered:b}:{text:B,isChecked:P});const T=v.name;k==="radio"&&T&&P&&t.querySelectorAll(`input[type="radio"][name="${T}"]`).forEach(G=>{if(G!==v){const V=G.value;m(G,c?{text:V,isChecked:!P,userTriggered:!1}:{text:V,isChecked:!P})}})}function m(S,v){const b=jt.get(S);if(!b||b.text!==v.text||b.isChecked!==v.isChecked){jt.set(S,v);const I=r.getId(S);_(e)(Object.assign(Object.assign({},v),{id:I}))}}const g=(u.input==="last"?["change"]:["input","change"]).map(S=>U(S,_(h),t)),p=t.defaultView;if(!p)return()=>{g.forEach(S=>S())};const y=p.Object.getOwnPropertyDescriptor(p.HTMLInputElement.prototype,"value"),w=[[p.HTMLInputElement.prototype,"value"],[p.HTMLInputElement.prototype,"checked"],[p.HTMLSelectElement.prototype,"value"],[p.HTMLTextAreaElement.prototype,"value"],[p.HTMLSelectElement.prototype,"selectedIndex"],[p.HTMLOptionElement.prototype,"selected"]];return y&&y.set&&g.push(...w.map(S=>Ne(S[0],S[1],{set(){_(h)({target:this,isTrusted:!1})}},!1,p))),_(()=>{g.forEach(S=>S())})}function Le(e){const t=[];function r(n,i){if(Fe("CSSGroupingRule")&&n.parentRule instanceof CSSGroupingRule||Fe("CSSMediaRule")&&n.parentRule instanceof CSSMediaRule||Fe("CSSSupportsRule")&&n.parentRule instanceof CSSSupportsRule||Fe("CSSConditionRule")&&n.parentRule instanceof CSSConditionRule){const a=Array.from(n.parentRule.cssRules).indexOf(n);i.unshift(a)}else if(n.parentStyleSheet){const a=Array.from(n.parentStyleSheet.cssRules).indexOf(n);i.unshift(a)}return i}return r(e,t)}function te(e,t,r){let n,i;return e?(e.ownerNode?n=t.getId(e.ownerNode):i=r.getId(e),{styleId:i,id:n}):{}}function sn({styleSheetRuleCb:e,mirror:t,stylesheetManager:r},{win:n}){if(!n.CSSStyleSheet||!n.CSSStyleSheet.prototype)return()=>{};const i=n.CSSStyleSheet.prototype.insertRule;n.CSSStyleSheet.prototype.insertRule=new Proxy(i,{apply:_((c,h,m)=>{const[f,g]=m,{id:p,styleId:y}=te(h,t,r.styleMirror);return(p&&p!==-1||y&&y!==-1)&&e({id:p,styleId:y,adds:[{rule:f,index:g}]}),c.apply(h,m)})});const o=n.CSSStyleSheet.prototype.deleteRule;n.CSSStyleSheet.prototype.deleteRule=new Proxy(o,{apply:_((c,h,m)=>{const[f]=m,{id:g,styleId:p}=te(h,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,removes:[{index:f}]}),c.apply(h,m)})});let a;n.CSSStyleSheet.prototype.replace&&(a=n.CSSStyleSheet.prototype.replace,n.CSSStyleSheet.prototype.replace=new Proxy(a,{apply:_((c,h,m)=>{const[f]=m,{id:g,styleId:p}=te(h,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,replace:f}),c.apply(h,m)})}));let l;n.CSSStyleSheet.prototype.replaceSync&&(l=n.CSSStyleSheet.prototype.replaceSync,n.CSSStyleSheet.prototype.replaceSync=new Proxy(l,{apply:_((c,h,m)=>{const[f]=m,{id:g,styleId:p}=te(h,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,replaceSync:f}),c.apply(h,m)})}));const s={};Pe("CSSGroupingRule")?s.CSSGroupingRule=n.CSSGroupingRule:(Pe("CSSMediaRule")&&(s.CSSMediaRule=n.CSSMediaRule),Pe("CSSConditionRule")&&(s.CSSConditionRule=n.CSSConditionRule),Pe("CSSSupportsRule")&&(s.CSSSupportsRule=n.CSSSupportsRule));const u={};return Object.entries(s).forEach(([c,h])=>{u[c]={insertRule:h.prototype.insertRule,deleteRule:h.prototype.deleteRule},h.prototype.insertRule=new Proxy(u[c].insertRule,{apply:_((m,f,g)=>{const[p,y]=g,{id:w,styleId:S}=te(f.parentStyleSheet,t,r.styleMirror);return(w&&w!==-1||S&&S!==-1)&&e({id:w,styleId:S,adds:[{rule:p,index:[...Le(f),y||0]}]}),m.apply(f,g)})}),h.prototype.deleteRule=new Proxy(u[c].deleteRule,{apply:_((m,f,g)=>{const[p]=g,{id:y,styleId:w}=te(f.parentStyleSheet,t,r.styleMirror);return(y&&y!==-1||w&&w!==-1)&&e({id:y,styleId:w,removes:[{index:[...Le(f),p]}]}),m.apply(f,g)})})}),_(()=>{n.CSSStyleSheet.prototype.insertRule=i,n.CSSStyleSheet.prototype.deleteRule=o,a&&(n.CSSStyleSheet.prototype.replace=a),l&&(n.CSSStyleSheet.prototype.replaceSync=l),Object.entries(s).forEach(([c,h])=>{h.prototype.insertRule=u[c].insertRule,h.prototype.deleteRule=u[c].deleteRule})})}function Gt({mirror:e,stylesheetManager:t},r){var n,i,o;let a=null;r.nodeName==="#document"?a=e.getId(r):a=e.getId(r.host);const l=r.nodeName==="#document"?(n=r.defaultView)===null||n===void 0?void 0:n.Document:(o=(i=r.ownerDocument)===null||i===void 0?void 0:i.defaultView)===null||o===void 0?void 0:o.ShadowRoot,s=l?.prototype?Object.getOwnPropertyDescriptor(l?.prototype,"adoptedStyleSheets"):void 0;return a===null||a===-1||!l||!s?()=>{}:(Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get(){var u;return(u=s.get)===null||u===void 0?void 0:u.call(this)},set(u){var c;const h=(c=s.set)===null||c===void 0?void 0:c.call(this,u);if(a!==null&&a!==-1)try{t.adoptStyleSheets(u,a)}catch{}return h}}),_(()=>{Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get:s.get,set:s.set})}))}function an({styleDeclarationCb:e,mirror:t,ignoreCSSAttributes:r,stylesheetManager:n},{win:i}){const o=i.CSSStyleDeclaration.prototype.setProperty;i.CSSStyleDeclaration.prototype.setProperty=new Proxy(o,{apply:_((l,s,u)=>{var c;const[h,m,f]=u;if(r.has(h))return o.apply(s,[h,m,f]);const{id:g,styleId:p}=te((c=s.parentRule)===null||c===void 0?void 0:c.parentStyleSheet,t,n.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,set:{property:h,value:m,priority:f},index:Le(s.parentRule)}),l.apply(s,u)})});const a=i.CSSStyleDeclaration.prototype.removeProperty;return i.CSSStyleDeclaration.prototype.removeProperty=new Proxy(a,{apply:_((l,s,u)=>{var c;const[h]=u;if(r.has(h))return a.apply(s,[h]);const{id:m,styleId:f}=te((c=s.parentRule)===null||c===void 0?void 0:c.parentStyleSheet,t,n.styleMirror);return(m&&m!==-1||f&&f!==-1)&&e({id:m,styleId:f,remove:{property:h},index:Le(s.parentRule)}),l.apply(s,u)})}),_(()=>{i.CSSStyleDeclaration.prototype.setProperty=o,i.CSSStyleDeclaration.prototype.removeProperty=a})}function ln({mediaInteractionCb:e,blockClass:t,blockSelector:r,mirror:n,sampling:i,doc:o}){const a=_(s=>Me(_(u=>{const c=_e(u);if(!c||H(c,t,r,!0))return;const{currentTime:h,volume:m,muted:f,playbackRate:g,loop:p}=c;e({type:s,id:n.getId(c),currentTime:h,volume:m,muted:f,playbackRate:g,loop:p})}),i.media||500)),l=[U("play",a(0),o),U("pause",a(1),o),U("seeked",a(2),o),U("volumechange",a(3),o),U("ratechange",a(4),o)];return _(()=>{l.forEach(s=>s())})}function un({fontCb:e,doc:t}){const r=t.defaultView;if(!r)return()=>{};const n=[],i=new WeakMap,o=r.FontFace;r.FontFace=function(s,u,c){const h=new o(s,u,c);return i.set(h,{family:s,buffer:typeof u!="string",descriptors:c,fontSource:typeof u=="string"?u:JSON.stringify(Array.from(new Uint8Array(u)))}),h};const a=ge(t.fonts,"add",function(l){return function(s){return setTimeout(_(()=>{const u=i.get(s);u&&(e(u),i.delete(s))}),0),l.apply(this,[s])}});return n.push(()=>{r.FontFace=o}),n.push(a),_(()=>{n.forEach(l=>l())})}function cn(e){const{doc:t,mirror:r,blockClass:n,blockSelector:i,selectionCb:o}=e;let a=!0;const l=_(()=>{const s=t.getSelection();if(!s||a&&s?.isCollapsed)return;a=s.isCollapsed||!1;const u=[],c=s.rangeCount||0;for(let h=0;h{}:ge(r.customElements,"define",function(i){return function(o,a,l){try{t({define:{name:o}})}catch{console.warn(`Custom element callback failed for ${o}`)}return i.apply(this,[o,a,l])}})}function fn(e,t){const{mutationCb:r,mousemoveCb:n,mouseInteractionCb:i,scrollCb:o,viewportResizeCb:a,inputCb:l,mediaInteractionCb:s,styleSheetRuleCb:u,styleDeclarationCb:c,canvasMutationCb:h,fontCb:m,selectionCb:f,customElementCb:g}=e;e.mutationCb=(...p)=>{t.mutation&&t.mutation(...p),r(...p)},e.mousemoveCb=(...p)=>{t.mousemove&&t.mousemove(...p),n(...p)},e.mouseInteractionCb=(...p)=>{t.mouseInteraction&&t.mouseInteraction(...p),i(...p)},e.scrollCb=(...p)=>{t.scroll&&t.scroll(...p),o(...p)},e.viewportResizeCb=(...p)=>{t.viewportResize&&t.viewportResize(...p),a(...p)},e.inputCb=(...p)=>{t.input&&t.input(...p),l(...p)},e.mediaInteractionCb=(...p)=>{t.mediaInteaction&&t.mediaInteaction(...p),s(...p)},e.styleSheetRuleCb=(...p)=>{t.styleSheetRule&&t.styleSheetRule(...p),u(...p)},e.styleDeclarationCb=(...p)=>{t.styleDeclaration&&t.styleDeclaration(...p),c(...p)},e.canvasMutationCb=(...p)=>{t.canvasMutation&&t.canvasMutation(...p),h(...p)},e.fontCb=(...p)=>{t.font&&t.font(...p),m(...p)},e.selectionCb=(...p)=>{t.selection&&t.selection(...p),f(...p)},e.customElementCb=(...p)=>{t.customElement&&t.customElement(...p),g(...p)}}function hn(e,t={}){const r=e.doc.defaultView;if(!r)return()=>{};fn(e,t);let n;e.recordDOM&&(n=qt(e,e.doc));const i=en(e),o=tn(e),a=$t(e),l=rn(e,{win:r}),s=on(e),u=ln(e);let c=()=>{},h=()=>{},m=()=>{},f=()=>{};e.recordDOM&&(c=sn(e,{win:r}),h=Gt(e,e.doc),m=an(e,{win:r}),e.collectFonts&&(f=un(e)));const g=cn(e),p=dn(e),y=[];for(const w of e.plugins)y.push(w.observer(w.callback,r,w.options));return _(()=>{oe.forEach(w=>w.reset()),n?.disconnect(),i(),o(),a(),l(),s(),u(),c(),h(),m(),f(),g(),p(),y.forEach(w=>w())})}function Fe(e){return typeof window[e]<"u"}function Pe(e){return!!(typeof window[e]<"u"&&window[e].prototype&&"insertRule"in window[e].prototype&&"deleteRule"in window[e].prototype)}class Vt{constructor(t){this.generateIdFn=t,this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap}getId(t,r,n,i){const o=n||this.getIdToRemoteIdMap(t),a=i||this.getRemoteIdToIdMap(t);let l=o.get(r);return l||(l=this.generateIdFn(),o.set(r,l),a.set(l,r)),l}getIds(t,r){const n=this.getIdToRemoteIdMap(t),i=this.getRemoteIdToIdMap(t);return r.map(o=>this.getId(t,o,n,i))}getRemoteId(t,r,n){const i=n||this.getRemoteIdToIdMap(t);if(typeof r!="number")return r;const o=i.get(r);return o||-1}getRemoteIds(t,r){const n=this.getRemoteIdToIdMap(t);return r.map(i=>this.getRemoteId(t,i,n))}reset(t){if(!t){this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap;return}this.iframeIdToRemoteIdMap.delete(t),this.iframeRemoteIdToIdMap.delete(t)}getIdToRemoteIdMap(t){let r=this.iframeIdToRemoteIdMap.get(t);return r||(r=new Map,this.iframeIdToRemoteIdMap.set(t,r)),r}getRemoteIdToIdMap(t){let r=this.iframeRemoteIdToIdMap.get(t);return r||(r=new Map,this.iframeRemoteIdToIdMap.set(t,r)),r}}class pn{constructor(t){this.iframes=new WeakMap,this.crossOriginIframeMap=new WeakMap,this.crossOriginIframeMirror=new Vt(It),this.crossOriginIframeRootIdMap=new WeakMap,this.mutationCb=t.mutationCb,this.wrappedEmit=t.wrappedEmit,this.stylesheetManager=t.stylesheetManager,this.recordCrossOriginIframes=t.recordCrossOriginIframes,this.crossOriginIframeStyleMirror=new Vt(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)),this.mirror=t.mirror,this.recordCrossOriginIframes&&window.addEventListener("message",this.handleMessage.bind(this))}addIframe(t){this.iframes.set(t,!0),t.contentWindow&&this.crossOriginIframeMap.set(t.contentWindow,t)}addLoadListener(t){this.loadListener=t}attachIframe(t,r){var n;this.mutationCb({adds:[{parentId:this.mirror.getId(t),nextId:null,node:r}],removes:[],texts:[],attributes:[],isAttachIframe:!0}),(n=this.loadListener)===null||n===void 0||n.call(this,t),t.contentDocument&&t.contentDocument.adoptedStyleSheets&&t.contentDocument.adoptedStyleSheets.length>0&&this.stylesheetManager.adoptStyleSheets(t.contentDocument.adoptedStyleSheets,this.mirror.getId(t.contentDocument))}handleMessage(t){const r=t;if(r.data.type!=="rrweb"||r.origin!==r.data.origin||!t.source)return;const i=this.crossOriginIframeMap.get(t.source);if(!i)return;const o=this.transformCrossOriginEvent(i,r.data.event);o&&this.wrappedEmit(o,r.data.isCheckout)}transformCrossOriginEvent(t,r){var n;switch(r.type){case E.FullSnapshot:{this.crossOriginIframeMirror.reset(t),this.crossOriginIframeStyleMirror.reset(t),this.replaceIdOnNode(r.data.node,t);const i=r.data.node.id;return this.crossOriginIframeRootIdMap.set(t,i),this.patchRootIdOnNode(r.data.node,i),{timestamp:r.timestamp,type:E.IncrementalSnapshot,data:{source:C.Mutation,adds:[{parentId:this.mirror.getId(t),nextId:null,node:r.data.node}],removes:[],texts:[],attributes:[],isAttachIframe:!0}}}case E.Meta:case E.Load:case E.DomContentLoaded:return!1;case E.Plugin:return r;case E.Custom:return this.replaceIds(r.data.payload,t,["id","parentId","previousId","nextId"]),r;case E.IncrementalSnapshot:switch(r.data.source){case C.Mutation:return r.data.adds.forEach(i=>{this.replaceIds(i,t,["parentId","nextId","previousId"]),this.replaceIdOnNode(i.node,t);const o=this.crossOriginIframeRootIdMap.get(t);o&&this.patchRootIdOnNode(i.node,o)}),r.data.removes.forEach(i=>{this.replaceIds(i,t,["parentId","id"])}),r.data.attributes.forEach(i=>{this.replaceIds(i,t,["id"])}),r.data.texts.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.Drag:case C.TouchMove:case C.MouseMove:return r.data.positions.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.ViewportResize:return!1;case C.MediaInteraction:case C.MouseInteraction:case C.Scroll:case C.CanvasMutation:case C.Input:return this.replaceIds(r.data,t,["id"]),r;case C.StyleSheetRule:case C.StyleDeclaration:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleId"]),r;case C.Font:return r;case C.Selection:return r.data.ranges.forEach(i=>{this.replaceIds(i,t,["start","end"])}),r;case C.AdoptedStyleSheet:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleIds"]),(n=r.data.styles)===null||n===void 0||n.forEach(i=>{this.replaceStyleIds(i,t,["styleId"])}),r}}return!1}replace(t,r,n,i){for(const o of i)!Array.isArray(r[o])&&typeof r[o]!="number"||(Array.isArray(r[o])?r[o]=t.getIds(n,r[o]):r[o]=t.getId(n,r[o]));return r}replaceIds(t,r,n){return this.replace(this.crossOriginIframeMirror,t,r,n)}replaceStyleIds(t,r,n){return this.replace(this.crossOriginIframeStyleMirror,t,r,n)}replaceIdOnNode(t,r){this.replaceIds(t,r,["id","rootId"]),"childNodes"in t&&t.childNodes.forEach(n=>{this.replaceIdOnNode(n,r)})}patchRootIdOnNode(t,r){t.type!==A.Document&&!t.rootId&&(t.rootId=r),"childNodes"in t&&t.childNodes.forEach(n=>{this.patchRootIdOnNode(n,r)})}}class mn{constructor(t){this.shadowDoms=new WeakSet,this.restoreHandlers=[],this.mutationCb=t.mutationCb,this.scrollCb=t.scrollCb,this.bypassOptions=t.bypassOptions,this.mirror=t.mirror,this.init()}init(){this.reset(),this.patchAttachShadow(Element,document)}addShadowRoot(t,r){if(!we(t)||this.shadowDoms.has(t))return;this.shadowDoms.add(t);const n=qt(Object.assign(Object.assign({},this.bypassOptions),{doc:r,mutationCb:this.mutationCb,mirror:this.mirror,shadowDomManager:this}),t);this.restoreHandlers.push(()=>n.disconnect()),this.restoreHandlers.push($t(Object.assign(Object.assign({},this.bypassOptions),{scrollCb:this.scrollCb,doc:t,mirror:this.mirror}))),setTimeout(()=>{t.adoptedStyleSheets&&t.adoptedStyleSheets.length>0&&this.bypassOptions.stylesheetManager.adoptStyleSheets(t.adoptedStyleSheets,this.mirror.getId(t.host)),this.restoreHandlers.push(Gt({mirror:this.mirror,stylesheetManager:this.bypassOptions.stylesheetManager},t))},0)}observeAttachShadow(t){!t.contentWindow||!t.contentDocument||this.patchAttachShadow(t.contentWindow.Element,t.contentDocument)}patchAttachShadow(t,r){const n=this;this.restoreHandlers.push(ge(t.prototype,"attachShadow",function(i){return function(o){const a=i.call(this,o);return this.shadowRoot&&Ft(this)&&n.addShadowRoot(this.shadowRoot,r),a}}))}reset(){this.restoreHandlers.forEach(t=>{try{t()}catch{}}),this.restoreHandlers=[],this.shadowDoms=new WeakSet}}/*! ***************************************************************************** ->>>>>>> 571d8b5 (cleanup, draft) Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any @@ -19,17 +13,10 @@ or you can use record.mirror to access the mirror instance during recording.`;le LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -<<<<<<< HEAD ***************************************************************************** */function an(e,t){var r={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var i=0,n=Object.getOwnPropertySymbols(e);i"u"?[]:new Uint8Array(256),Ae=0;Ae>2],i+=me[(t[r]&3)<<4|t[r+1]>>4],i+=me[(t[r+1]&15)<<2|t[r+2]>>6],i+=me[t[r+2]&63];return n%3===2?i=i.substring(0,i.length-1)+"=":n%3===1&&(i=i.substring(0,i.length-2)+"=="),i};const zt=new Map;function dn(e,t){let r=zt.get(e);return r||(r=new Map,zt.set(e,r)),r.has(t)||r.set(t,[]),r.get(t)}const $t=(e,t,r)=>{if(!e||!(jt(e,t)||typeof e=="object"))return;const n=e.constructor.name,i=dn(r,n);let o=i.indexOf(e);return o===-1&&(o=i.length,i.push(e)),o};function Le(e,t,r){if(e instanceof Array)return e.map(n=>Le(n,t,r));if(e===null)return e;if(e instanceof Float32Array||e instanceof Float64Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Uint8Array||e instanceof Uint16Array||e instanceof Int16Array||e instanceof Int8Array||e instanceof Uint8ClampedArray)return{rr_type:e.constructor.name,args:[Object.values(e)]};if(e instanceof ArrayBuffer){const n=e.constructor.name,i=un(e);return{rr_type:n,base64:i}}else{if(e instanceof DataView)return{rr_type:e.constructor.name,args:[Le(e.buffer,t,r),e.byteOffset,e.byteLength]};if(e instanceof HTMLImageElement){const n=e.constructor.name,{src:i}=e;return{rr_type:n,src:i}}else if(e instanceof HTMLCanvasElement){const n="HTMLImageElement",i=e.toDataURL();return{rr_type:n,src:i}}else{if(e instanceof ImageData)return{rr_type:e.constructor.name,args:[Le(e.data,t,r),e.width,e.height]};if(jt(e,t)||typeof e=="object"){const n=e.constructor.name,i=$t(e,t,r);return{rr_type:n,index:i}}}}return e}const Gt=(e,t,r)=>e.map(n=>Le(n,t,r)),jt=(e,t)=>!!["WebGLActiveInfo","WebGLBuffer","WebGLFramebuffer","WebGLProgram","WebGLRenderbuffer","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebGLVertexArrayObject","WebGLVertexArrayObjectOES"].filter(i=>typeof t[i]=="function").find(i=>e instanceof t[i]);function fn(e,t,r,n){const i=[],o=Object.getOwnPropertyNames(t.CanvasRenderingContext2D.prototype);for(const a of o)try{if(typeof t.CanvasRenderingContext2D.prototype[a]!="function")continue;const l=he(t.CanvasRenderingContext2D.prototype,a,function(s){return function(...c){return U(this.canvas,r,n,!0)||setTimeout(()=>{const u=Gt(c,t,this);e(this.canvas,{type:pe["2D"],property:a,args:u})},0),s.apply(this,c)}});i.push(l)}catch{const s=ke(t.CanvasRenderingContext2D.prototype,a,{set(c){e(this.canvas,{type:pe["2D"],property:a,args:[c],setter:!0})}});i.push(s)}return()=>{i.forEach(a=>a())}}function hn(e){return e==="experimental-webgl"?"webgl":e}function Vt(e,t,r,n){const i=[];try{const o=he(e.HTMLCanvasElement.prototype,"getContext",function(a){return function(l,...s){if(!U(this,t,r,!0)){const c=hn(l);if("__context"in this||(this.__context=c),n&&["webgl","webgl2"].includes(c))if(s[0]&&typeof s[0]=="object"){const u=s[0];u.preserveDrawingBuffer||(u.preserveDrawingBuffer=!0)}else s.splice(0,1,{preserveDrawingBuffer:!0})}return a.apply(this,[l,...s])}});i.push(o)}catch{console.error("failed to patch HTMLCanvasElement.prototype.getContext")}return()=>{i.forEach(o=>o())}}function qt(e,t,r,n,i,o,a){const l=[],s=Object.getOwnPropertyNames(e);for(const c of s)if(!["isContextLost","canvas","drawingBufferWidth","drawingBufferHeight"].includes(c))try{if(typeof e[c]!="function")continue;const u=he(e,c,function(p){return function(...m){const f=p.apply(this,m);if($t(f,a,this),"tagName"in this.canvas&&!U(this.canvas,n,i,!0)){const g=Gt(m,a,this),h={type:t,property:c,args:g};r(this.canvas,h)}return f}});l.push(u)}catch{const p=ke(e,c,{set(m){r(this.canvas,{type:t,property:c,args:[m],setter:!0})}});l.push(p)}return l}function pn(e,t,r,n,i){const o=[];return o.push(...qt(t.WebGLRenderingContext.prototype,pe.WebGL,e,r,n,i,t)),typeof t.WebGL2RenderingContext<"u"&&o.push(...qt(t.WebGL2RenderingContext.prototype,pe.WebGL2,e,r,n,i,t)),()=>{o.forEach(a=>a())}}function mn(e,t){var r=t===void 0?null:t,n=e.toString(),i=n.split(` `);i.pop(),i.shift();for(var o=i[0].search(/\S/),a=/(['"])__worker_loader_strict__(['"])/g,l=0,s=i.length;l"u"?[]:new Uint8Array(256),Be=0;Be>2],i+=ve[(t[r]&3)<<4|t[r+1]>>4],i+=ve[(t[r+1]&15)<<2|t[r+2]>>6],i+=ve[t[r+2]&63];return n%3===2?i=i.substring(0,i.length-1)+"=":n%3===1&&(i=i.substring(0,i.length-2)+"=="),i};const Jt=new Map;function bn(e,t){let r=Jt.get(e);return r||(r=new Map,Jt.set(e,r)),r.has(t)||r.set(t,[]),r.get(t)}const Xt=(e,t,r)=>{if(!e||!(Yt(e,t)||typeof e=="object"))return;const n=e.constructor.name,i=bn(r,n);let o=i.indexOf(e);return o===-1&&(o=i.length,i.push(e)),o};function We(e,t,r){if(e instanceof Array)return e.map(n=>We(n,t,r));if(e===null)return e;if(e instanceof Float32Array||e instanceof Float64Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Uint8Array||e instanceof Uint16Array||e instanceof Int16Array||e instanceof Int8Array||e instanceof Uint8ClampedArray)return{rr_type:e.constructor.name,args:[Object.values(e)]};if(e instanceof ArrayBuffer){const n=e.constructor.name,i=Sn(e);return{rr_type:n,base64:i}}else{if(e instanceof DataView)return{rr_type:e.constructor.name,args:[We(e.buffer,t,r),e.byteOffset,e.byteLength]};if(e instanceof HTMLImageElement){const n=e.constructor.name,{src:i}=e;return{rr_type:n,src:i}}else if(e instanceof HTMLCanvasElement){const n="HTMLImageElement",i=e.toDataURL();return{rr_type:n,src:i}}else{if(e instanceof ImageData)return{rr_type:e.constructor.name,args:[We(e.data,t,r),e.width,e.height]};if(Yt(e,t)||typeof e=="object"){const n=e.constructor.name,i=Xt(e,t,r);return{rr_type:n,index:i}}}}return e}const Kt=(e,t,r)=>e.map(n=>We(n,t,r)),Yt=(e,t)=>!!["WebGLActiveInfo","WebGLBuffer","WebGLFramebuffer","WebGLProgram","WebGLRenderbuffer","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebGLVertexArrayObject","WebGLVertexArrayObjectOES"].filter(i=>typeof t[i]=="function").find(i=>e instanceof t[i]);function wn(e,t,r,n){const i=[],o=Object.getOwnPropertyNames(t.CanvasRenderingContext2D.prototype);for(const a of o)try{if(typeof t.CanvasRenderingContext2D.prototype[a]!="function")continue;const l=ge(t.CanvasRenderingContext2D.prototype,a,function(s){return function(...u){return H(this.canvas,r,n,!0)||setTimeout(()=>{const c=Kt(u,t,this);e(this.canvas,{type:ye["2D"],property:a,args:c})},0),s.apply(this,u)}});i.push(l)}catch{const s=Ne(t.CanvasRenderingContext2D.prototype,a,{set(u){e(this.canvas,{type:ye["2D"],property:a,args:[u],setter:!0})}});i.push(s)}return()=>{i.forEach(a=>a())}}function In(e){return e==="experimental-webgl"?"webgl":e}function Qt(e,t,r,n){const i=[];try{const o=ge(e.HTMLCanvasElement.prototype,"getContext",function(a){return function(l,...s){if(!H(this,t,r,!0)){const u=In(l);if("__context"in this||(this.__context=u),n&&["webgl","webgl2"].includes(u))if(s[0]&&typeof s[0]=="object"){const c=s[0];c.preserveDrawingBuffer||(c.preserveDrawingBuffer=!0)}else s.splice(0,1,{preserveDrawingBuffer:!0})}return a.apply(this,[l,...s])}});i.push(o)}catch{console.error("failed to patch HTMLCanvasElement.prototype.getContext")}return()=>{i.forEach(o=>o())}}function Zt(e,t,r,n,i,o,a){const l=[],s=Object.getOwnPropertyNames(e);for(const u of s)if(!["isContextLost","canvas","drawingBufferWidth","drawingBufferHeight"].includes(u))try{if(typeof e[u]!="function")continue;const c=ge(e,u,function(h){return function(...m){const f=h.apply(this,m);if(Xt(f,a,this),"tagName"in this.canvas&&!H(this.canvas,n,i,!0)){const g=Kt(m,a,this),p={type:t,property:u,args:g};r(this.canvas,p)}return f}});l.push(c)}catch{const h=Ne(e,u,{set(m){r(this.canvas,{type:t,property:u,args:[m],setter:!0})}});l.push(h)}return l}function Mn(e,t,r,n,i){const o=[];return o.push(...Zt(t.WebGLRenderingContext.prototype,ye.WebGL,e,r,n,i,t)),typeof t.WebGL2RenderingContext<"u"&&o.push(...Zt(t.WebGL2RenderingContext.prototype,ye.WebGL2,e,r,n,i,t)),()=>{o.forEach(a=>a())}}function Cn(e,t){var r=t===void 0?null:t,n=e.toString(),i=n.split(` -`);i.pop(),i.shift();for(var o=i[0].search(/\S/),a=/(['"])__worker_loader_strict__(['"])/g,l=0,s=i.length;l>>>>>> 571d8b5 (cleanup, draft) Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any @@ -42,7 +29,6 @@ or you can use record.mirror to access the mirror instance during recording.`;le LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -<<<<<<< HEAD ***************************************************************************** */function e(c,u,p,m){function f(g){return g instanceof p?g:new p(function(h){h(g)})}return new(p||(p=Promise))(function(g,h){function y(S){try{v(m.next(S))}catch(b){h(b)}}function w(S){try{v(m.throw(S))}catch(b){h(b)}}function v(S){S.done?g(S.value):f(S.value).then(y,w)}v((m=m.apply(c,u||[])).next())})}for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=typeof Uint8Array>"u"?[]:new Uint8Array(256),n=0;n>2],f+=t[(u[p]&3)<<4|u[p+1]>>4],f+=t[(u[p+1]&15)<<2|u[p+2]>>6],f+=t[u[p+2]&63];return m%3===2?f=f.substring(0,f.length-1)+"=":m%3===1&&(f=f.substring(0,f.length-2)+"=="),f};const o=new Map,a=new Map;function l(c,u,p){return e(this,void 0,void 0,function*(){const m=`${c}-${u}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const f=new OffscreenCanvas(c,u);f.getContext("2d");const h=yield(yield f.convertToBlob(p)).arrayBuffer(),y=i(h);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(c){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:u,bitmap:p,width:m,height:f,dataURLOptions:g}=c.data,h=l(m,f,g),y=new OffscreenCanvas(m,f);y.getContext("2d").drawImage(p,0,0),p.close();const v=yield y.convertToBlob(g),S=v.type,b=yield v.arrayBuffer(),M=i(b);if(!o.has(u)&&(yield h)===M)return o.set(u,M),s.postMessage({id:u});if(o.get(u)===M)return s.postMessage({id:u});s.postMessage({id:u,type:S,base64:M,width:m,height:f}),o.set(u,M)}else return s.postMessage({id:c.data.id})})}})()},null);class vn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,c)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(c)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Vt(r,n,i,!0),l=new Map,s=new Sn;s.onmessage=g=>{const{id:h}=g.data;if(l.set(h,!1),!("base64"in g.data))return;const{base64:y,type:w,width:v,height:S}=g.data;this.mutationCb({id:h,type:pe["2D"],commands:[{property:"clearRect",args:[0,0,v,S]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const c=1e3/t;let u=0,p;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(h=>{U(h,n,i,!0)||g.push(h)}),g},f=g=>{if(u&&g-uln(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(h);if(l.get(w)||h.width===0||h.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(h.__context)){const S=h.getContext(h.__context);((y=S?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&S.clear(S.COLOR_BUFFER_BIT)}const v=yield createImageBitmap(h);s.postMessage({id:w,bitmap:v,width:h.width,height:h.height,dataURLOptions:o.dataURLOptions},[v])})),p=requestAnimationFrame(f)};p=requestAnimationFrame(f),this.resetObservers=()=>{a(),cancelAnimationFrame(p)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Vt(t,r,n,!1),o=fn(this.processMutation.bind(this),t,r,n),a=pn(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>an(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class bn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Br,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:ft(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class wn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Te()})}let D,Pe,Ze,Fe=!1;const q=hr();function Me(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:c=null,inlineStylesheet:u=!0,maskAllInputs:p,maskInputOptions:m,slimDOMOptions:f,maskInputFn:g,maskTextFn:h,hooks:y,packFn:w,sampling:v={},dataURLOptions:S={},mousemoveWait:b,recordDOM:M=!0,recordCanvas:F=!1,recordCrossOriginIframes:P=!1,recordAfter:k=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:T=!1,collectFonts:j=!1,inlineImages:V=!1,plugins:x,keepIframeSrcFn:oe=()=>!1,ignoreCSSAttributes:H=new Set([]),errorHandler:ee}=e;$r(ee);const K=P?window.parent===window:!0;let $e=!1;if(!K)try{window.parent.document&&($e=!1)}catch{$e=!0}if(K&&!t)throw new Error("emit function is required");b!==void 0&&v.mousemove===void 0&&(v.mousemove=b),q.reset();const at=p===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},lt=f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:f==="all",headMetaDescKeywords:f==="all"}:f||{};Fr();let nr,ct=0;const ir=I=>{for(const X of x||[])X.eventProcessor&&(I=X.eventProcessor(I));return w&&!$e&&(I=w(I)),I};D=(I,X)=>{var N;if(!((N=re[0])===null||N===void 0)&&N.isFrozen()&&I.type!==_.FullSnapshot&&!(I.type===_.IncrementalSnapshot&&I.data.source===C.Mutation)&&re.forEach(z=>z.unfreeze()),K)t?.(ir(I),X);else if($e){const z={type:"rrweb",event:ir(I),origin:window.location.origin,isCheckout:X};window.parent.postMessage(z,"*")}if(I.type===_.FullSnapshot)nr=I,ct=0;else if(I.type===_.IncrementalSnapshot){if(I.data.source===C.Mutation&&I.data.isAttachIframe)return;ct++;const z=n&&ct>=n,le=r&&I.timestamp-nr.timestamp>r;(z||le)&&Pe(!0)}};const Ge=I=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Mutation},I)}))},or=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Scroll},I)})),sr=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},I)})),Un=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},I)})),se=new bn({mutationCb:Ge,adoptedStyleSheetCb:Un}),ae=new on({mirror:q,mutationCb:Ge,stylesheetManager:se,recordCrossOriginIframes:P,wrappedEmit:D});for(const I of x||[])I.getMirror&&I.getMirror({nodeMirror:q,crossOriginIframeMirror:ae.crossOriginIframeMirror,crossOriginIframeStyleMirror:ae.crossOriginIframeStyleMirror});const ut=new wn;Ze=new vn({recordCanvas:F,mutationCb:sr,win:window,blockClass:i,blockSelector:o,mirror:q,sampling:v.canvas,dataURLOptions:S});const je=new sn({mutationCb:Ge,scrollCb:or,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskInputOptions:at,dataURLOptions:S,maskTextFn:h,maskInputFn:g,recordCanvas:F,inlineImages:V,sampling:v,slimDOMOptions:lt,iframeManager:ae,stylesheetManager:se,canvasManager:Ze,keepIframeSrcFn:oe,processedNodeManager:ut},mirror:q});Pe=(I=!1)=>{if(!M)return;D(L({type:_.Meta,data:{href:window.location.href,width:Ct(),height:It()}}),I),se.reset(),je.init(),re.forEach(N=>N.lock());const X=Lr(document,{mirror:q,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskAllInputs:at,maskTextFn:h,slimDOM:lt,dataURLOptions:S,recordCanvas:F,inlineImages:V,onSerialize:N=>{xt(N,q)&&ae.addIframe(N),Et(N,q)&&se.trackLinkElement(N),Ye(N)&&je.addShadowRoot(N.shadowRoot,document)},onIframeLoad:(N,z)=>{ae.attachIframe(N,z),je.observeAttachShadow(N)},onStylesheetLoad:(N,z)=>{se.attachLinkElement(N,z)},keepIframeSrcFn:oe});if(!X)return console.warn("Failed to snapshot the document");D(L({type:_.FullSnapshot,data:{node:X,initialOffset:Mt(window)}}),I),re.forEach(N=>N.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&se.adoptStyleSheets(document.adoptedStyleSheets,q.getId(document))};try{const I=[],X=z=>{var le;return O(nn)({mutationCb:Ge,mousemoveCb:(R,dt)=>D(L({type:_.IncrementalSnapshot,data:{source:dt,positions:R}})),mouseInteractionCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},R)})),scrollCb:or,viewportResizeCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},R)})),inputCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Input},R)})),mediaInteractionCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},R)})),styleSheetRuleCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},R)})),styleDeclarationCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},R)})),canvasMutationCb:sr,fontCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Font},R)})),selectionCb:R=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Selection},R)}))},customElementCb:R=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},R)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:c,maskInputOptions:at,inlineStylesheet:u,sampling:v,recordDOM:M,recordCanvas:F,inlineImages:V,userTriggeredOnInput:T,collectFonts:j,doc:z,maskInputFn:g,maskTextFn:h,keepIframeSrcFn:oe,blockSelector:o,slimDOMOptions:lt,dataURLOptions:S,mirror:q,iframeManager:ae,stylesheetManager:se,shadowDomManager:je,processedNodeManager:ut,canvasManager:Ze,ignoreCSSAttributes:H,plugins:((le=x?.filter(R=>R.observer))===null||le===void 0?void 0:le.map(R=>({observer:R.observer,options:R.options,callback:dt=>D(L({type:_.Plugin,data:{plugin:R.name,payload:dt}}))})))||[]},y)};ae.addLoadListener(z=>{try{I.push(X(z.contentDocument))}catch(le){console.warn(le)}});const N=()=>{Pe(),I.push(X(document)),Fe=!0};return document.readyState==="interactive"||document.readyState==="complete"?N():(I.push(W("DOMContentLoaded",()=>{D(L({type:_.DomContentLoaded,data:{}})),k==="DOMContentLoaded"&&N()})),I.push(W("load",()=>{D(L({type:_.Load,data:{}})),k==="load"&&N()},window))),()=>{I.forEach(z=>z()),ut.destroy(),Fe=!1,Gr()}}catch(I){console.warn(I)}}Me.addCustomEvent=(e,t)=>{if(!Fe)throw new Error("please add custom event after start recording");D(L({type:_.Custom,data:{tag:e,payload:t}}))},Me.freezePage=()=>{re.forEach(e=>e.freeze())},Me.takeFullSnapshot=e=>{if(!Fe)throw new Error("please take full snapshot after start recording");Pe(e)},Me.mirror=q;var Mn={DEBUG:!1,LIB_VERSION:"2.53.0"},B;if(typeof window>"u"){var Jt={hostname:""};B={navigator:{userAgent:""},document:{location:Jt,referrer:""},screen:{width:0,height:0},location:Jt}}else B=window;var Be=24*60*60*1e3,We=Array.prototype,In=Function.prototype,Xt=Object.prototype,ne=We.slice,Ie=Xt.toString,Ue=Xt.hasOwnProperty,Ce=B.console,Oe=B.navigator,G=B.document,He=B.opera,ze=B.screen,ie=Oe.userAgent,et=In.bind,Kt=We.forEach,Yt=We.indexOf,Qt=We.map,Cn=Array.isArray,tt={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},J={log:function(){},warn:function(){},error:function(){},critical:function(){if(!d.isUndefined(Ce)&&Ce){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{Ce.error.apply(Ce,e)}catch{d.each(e,function(r){Ce.error(r)})}}}},rt=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(J,arguments)}},On=function(e){return{log:rt(J.log,e),error:rt(J.error,e),critical:rt(J.critical,e)}};d.bind=function(e,t){var r,n;if(et&&e.bind===et)return et.apply(e,ne.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=ne.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(ne.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(ne.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(Kt&&e.forEach===Kt)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,c="",u="",p=0,m=a,f=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,f=[],Ie.apply(g)==="[object Array]"){for(p=g.length,s=0;s="0"&&t<="9";)g+=t,o();if(t===".")for(g+=".";o()&&t>="0"&&t<="9";)g+=t;if(t==="e"||t==="E")for(g+=t,o(),(t==="-"||t==="+")&&(g+=t,o());t>="0"&&t<="9";)g+=t,o();if(f=+g,!isFinite(f))i("Bad number");else return f},l=function(){var f,g,h="",y;if(t==='"')for(;o();){if(t==='"')return o(),h;if(t==="\\")if(o(),t==="u"){for(y=0,g=0;g<4&&(f=parseInt(o(),16),!!isFinite(f));g+=1)y=y*16+f;h+=String.fromCharCode(y)}else if(typeof r[t]=="string")h+=r[t];else break;else h+=t}i("Bad string")},s=function(){for(;t&&t<=" ";)o()},c=function(){switch(t){case"t":return o("t"),o("r"),o("u"),o("e"),!0;case"f":return o("f"),o("a"),o("l"),o("s"),o("e"),!1;case"n":return o("n"),o("u"),o("l"),o("l"),null}i('Unexpected "'+t+'"')},u,p=function(){var f=[];if(t==="["){if(o("["),s(),t==="]")return o("]"),f;for(;t;){if(f.push(u()),s(),t==="]")return o("]"),f;o(","),s()}}i("Bad array")},m=function(){var f,g={};if(t==="{"){if(o("{"),s(),t==="}")return o("}"),g;for(;t;){if(f=l(),s(),o(":"),Object.hasOwnProperty.call(g,f)&&i('Duplicate key "'+f+'"'),g[f]=u(),s(),t==="}")return o("}"),g;o(","),s()}}i("Bad object")};return u=function(){switch(s(),t){case"{":return m();case"[":return p();case'"':return l();case"-":return a();default:return t>="0"&&t<="9"?a():c()}},function(f){var g;return n=f,e=0,t=" ",g=u(),s(),t&&i("Syntax error"),g}}(),d.base64Encode=function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r,n,i,o,a,l,s,c,u=0,p=0,m="",f=[];if(!e)return e;e=d.utf8Encode(e);do r=e.charCodeAt(u++),n=e.charCodeAt(u++),i=e.charCodeAt(u++),c=r<<16|n<<8|i,o=c>>18&63,a=c>>12&63,l=c>>6&63,s=c&63,f[p++]=t.charAt(o)+t.charAt(a)+t.charAt(l)+t.charAt(s);while(u127&&a<2048?l=String.fromCharCode(a>>6|192,a&63|128):l=String.fromCharCode(a>>12|224,a>>6&63|128,a&63|128),l!==null&&(n>r&&(t+=e.substring(r,n)),t+=l,r=n=o+1)}return n>r&&(t+=e.substring(r,e.length)),t},d.UUID=function(){var e=function(){var n=1*new Date,i;if(B.performance&&B.performance.now)i=B.performance.now();else for(i=0;n==1*new Date;)i++;return n.toString(16)+Math.floor(i).toString(16)},t=function(){return Math.random().toString(16).replace(".","")},r=function(){var n=ie,i,o,a=[],l=0;function s(c,u){var p,m=0;for(p=0;p=4&&(l=s(l,a),a=[]);return a.length>0&&(l=s(l,a)),l.toString(16)};return function(){var n=(ze.height*ze.width).toString(16);return e()+"-"+t()+"-"+r()+"-"+n+"-"+e()}}();var Zt=["ahrefsbot","ahrefssiteaudit","baiduspider","bingbot","bingpreview","chrome-lighthouse","facebookexternal","petalbot","pinterest","screaming frog","yahoo! slurp","yandexbot","adsbot-google","apis-google","duplexweb-google","feedfetcher-google","google favicon","google web preview","google-read-aloud","googlebot","googleweblight","mediapartners-google","storebot-google"];d.isBlockedUA=function(e){var t;for(e=e.toLowerCase(),t=0;t=0}function n(i){if(!G.getElementsByTagName)return[];var o=i.split(" "),a,l,s,c,u,p,m,f,g,h,y=[G];for(p=0;p-1){l=a.split("#"),s=l[0];var w=l[1],v=G.getElementById(w);if(!v||s&&v.nodeName.toLowerCase()!=s)return[];y=[v];continue}if(a.indexOf(".")>-1){l=a.split("."),s=l[0];var S=l[1];for(s||(s="*"),c=[],u=0,m=0;m-1};break;default:k=function(T){return T.getAttribute(M)}}for(y=[],h=0,m=0;m=3?t[2]:""},currentUrl:function(){return B.location.href},properties:function(e){return typeof e!="object"&&(e={}),d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ie,Oe.vendor,He),$referrer:G.referrer,$referring_domain:d.info.referringDomain(G.referrer),$device:d.info.device(ie)}),{$current_url:d.info.currentUrl(),$browser_version:d.info.browserVersion(ie,Oe.vendor,He),$screen_height:ze.height,$screen_width:ze.width,mp_lib:"web",$lib_version:Mn.LIB_VERSION,$insert_id:er(),time:d.timestamp()/1e3},d.strip_empty_properties(e))},people_properties:function(){return d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ie,Oe.vendor,He)}),{$browser_version:d.info.browserVersion(ie,Oe.vendor,He)})},mpPageViewProperties:function(){return d.strip_empty_properties({current_page_title:G.title,current_domain:B.location.hostname,current_url_path:B.location.pathname,current_url_protocol:B.location.protocol,current_url_search:B.location.search})}};var er=function(e){var t=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return e?t.substring(0,e):t},Tn=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Rn=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,tr=function(e){var t=Rn,r=e.split("."),n=r[r.length-1];(n.length>4||n==="com"||n==="org")&&(t=Tn);var i=e.match(t);return i?i[0]:""},it=null,ot=null;typeof JSON<"u"&&(it=JSON.stringify,ot=JSON.parse),it=it||d.JSONEncode,ot=ot||d.JSONDecode,d.toArray=d.toArray,d.isObject=d.isObject,d.JSONEncode=d.JSONEncode,d.JSONDecode=d.JSONDecode,d.isBlockedUA=d.isBlockedUA,d.isEmptyObject=d.isEmptyObject,d.info=d.info,d.info.device=d.info.device,d.info.browser=d.info.browser,d.info.browserVersion=d.info.browserVersion,d.info.properties=d.info.properties;var Nn="__mp_opt_in_out_";function Dn(e,t){if(Bn(t))return J.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),!0;var r=Fn(e,t)==="0";return r&&J.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."),r}function An(e){return Wn(e,function(t){return this.get_config(t)})}function Ln(e){return e=e||{},e.persistenceType==="localStorage"?d.localStorage:d.cookie}function Pn(e,t){return t=t||{},(t.persistencePrefix||Nn)+e}function Fn(e,t){return Ln(t).get(Pn(e,t))}function Bn(e){if(e&&e.ignoreDnt)return!1;var t=e&&e.window||B,r=t.navigator||{},n=!1;return d.each([r.doNotTrack,r.msDoNotTrack,t.doNotTrack],function(i){d.includes([!0,1,"1","yes"],i)&&(n=!0)}),n}function Wn(e,t){return function(){var r=!1;try{var n=t.call(this,"token"),i=t.call(this,"ignore_dnt"),o=t.call(this,"opt_out_tracking_persistence_type"),a=t.call(this,"opt_out_tracking_cookie_prefix"),l=t.call(this,"window");n&&(r=Dn(n,{ignoreDnt:i,persistenceType:o,persistencePrefix:a,window:l}))}catch(c){J.error("Unexpected error when checking tracking opt-out status: "+c)}if(!r)return e.apply(this,arguments);var s=arguments[arguments.length-1];typeof s=="function"&&s(0)}}var st=On("recorder"),rr=window.CompressionStream,Q=function(e){this._mixpanel=e,this._stopRecording=null,this.recEvents=[],this.seqNo=0,this.replayId=null,this.replayStartTime=null,this.batchStartTime=null,this.replayLengthMs=0,this.sendBatchId=null,this.idleTimeoutId=null,this.maxTimeoutId=null,this.recordMaxMs=Be};Q.prototype.get_config=function(e){return this._mixpanel.get_config(e)},Q.prototype.startRecording=function(){if(this._stopRecording!==null){st.log("Recording already in progress, skipping startRecording.");return}this.recordMaxMs=this.get_config("record_max_ms"),this.recordMaxMs>Be&&(this.recordMaxMs=Be,st.critical("record_max_ms cannot be greater than "+Be+"ms. Capping value.")),this.recEvents=[],this.seqNo=0,this.startDate=new Date,this.replayStartTime=this.startDate.getTime(),this.batchStartTime=this.replayStartTime,this.replayId=d.UUID(),this.replayLengthMs=0;var e=d.bind(function(){clearTimeout(this.idleTimeoutId),this.idleTimeoutId=setTimeout(d.bind(function(){st.log("Idle timeout reached, restarting recording."),this.resetRecording()},this),this.get_config("record_idle_timeout_ms"))},this);this._stopRecording=Me({emit:d.bind(function(t){this.recEvents.push(t),this.replayLengthMs=new Date().getTime()-this.replayStartTime,e()},this),maskAllInputs:!0,maskTextSelector:this.get_config("record_mask_text_selector"),blockSelector:this.get_config("record_block_selector"),maskTextClass:this.get_config("record_mask_text_class"),blockClass:this.get_config("record_block_class")}),e(),this.sendBatchId=setInterval(d.bind(this.flushEventsWithOptOut,this),1e4),this.maxTimeoutId=setTimeout(d.bind(this.resetRecording,this),this.recordMaxMs)},Q.prototype.resetRecording=function(){this.stopRecording(),this.startRecording()},Q.prototype.stopRecording=function(){this._stopRecording!==null&&(this._stopRecording(),this._stopRecording=null),this._flushEvents(),this.replayId=null,clearInterval(this.sendBatchId),clearTimeout(this.idleTimeoutId),clearTimeout(this.maxTimeoutId)},Q.prototype.flushEventsWithOptOut=function(){this._flushEvents(d.bind(this._onOptOut,this))},Q.prototype._onOptOut=function(e){e===0&&(this.recEvents=[],this.stopRecording())},Q.prototype._sendRequest=function(e,t){window.fetch(this.get_config("api_host")+"/"+this.get_config("api_routes").record+"?"+new URLSearchParams(e),{method:"POST",headers:{Authorization:"Basic "+btoa(this.get_config("token")+":"),"Content-Type":"application/octet-stream"},body:t})},Q.prototype._flushEvents=An(function(){var e=this.recEvents.length;if(e>0){var t={distinct_id:String(this._mixpanel.get_distinct_id()),seq:this.seqNo++,batch_start_time:this.batchStartTime/1e3,replay_id:this.replayId,replay_length_ms:this.replayLengthMs,replay_start_time:this.replayStartTime/1e3},r=d.JSONEncode(this.recEvents),n=this._mixpanel.get_property("$device_id");n&&(t.$device_id=n);var i=this._mixpanel.get_property("$user_id");if(i&&(t.$user_id=i),this.recEvents=this.recEvents.slice(e),this.batchStartTime=new Date().getTime(),rr){var o=new Blob([r],{type:"application/json"}).stream(),a=o.pipeThrough(new rr("gzip"));new Response(a).blob().then(d.bind(function(l){t.format="gzip",this._sendRequest(t,l)},this))}else t.format="body",this._sendRequest(t,r)}}),window.__mp_recorder=Q})(); -======= - ***************************************************************************** */function e(u,c,h,m){function f(g){return g instanceof h?g:new h(function(p){p(g)})}return new(h||(h=Promise))(function(g,p){function y(v){try{S(m.next(v))}catch(b){p(b)}}function w(v){try{S(m.throw(v))}catch(b){p(b)}}function S(v){v.done?g(v.value):f(v.value).then(y,w)}S((m=m.apply(u,c||[])).next())})}for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=typeof Uint8Array>"u"?[]:new Uint8Array(256),n=0;n>2],f+=t[(c[h]&3)<<4|c[h+1]>>4],f+=t[(c[h+1]&15)<<2|c[h+2]>>6],f+=t[c[h+2]&63];return m%3===2?f=f.substring(0,f.length-1)+"=":m%3===1&&(f=f.substring(0,f.length-2)+"=="),f};const o=new Map,a=new Map;function l(u,c,h){return e(this,void 0,void 0,function*(){const m=`${u}-${c}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const f=new OffscreenCanvas(u,c);f.getContext("2d");const p=yield(yield f.convertToBlob(h)).arrayBuffer(),y=i(p);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(u){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:c,bitmap:h,width:m,height:f,dataURLOptions:g}=u.data,p=l(m,f,g),y=new OffscreenCanvas(m,f);y.getContext("2d").drawImage(h,0,0),h.close();const S=yield y.convertToBlob(g),v=S.type,b=yield S.arrayBuffer(),I=i(b);if(!o.has(c)&&(yield p)===I)return o.set(c,I),s.postMessage({id:c});if(o.get(c)===I)return s.postMessage({id:c});s.postMessage({id:c,type:v,base64:I,width:m,height:f}),o.set(c,I)}else return s.postMessage({id:u.data.id})})}})()},null);class xn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,u)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(u)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Qt(r,n,i,!0),l=new Map,s=new On;s.onmessage=g=>{const{id:p}=g.data;if(l.set(p,!1),!("base64"in g.data))return;const{base64:y,type:w,width:S,height:v}=g.data;this.mutationCb({id:p,type:ye["2D"],commands:[{property:"clearRect",args:[0,0,S,v]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const u=1e3/t;let c=0,h;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(p=>{H(p,n,i,!0)||g.push(p)}),g},f=g=>{if(c&&g-cyn(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(p);if(l.get(w)||p.width===0||p.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(p.__context)){const v=p.getContext(p.__context);((y=v?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&v.clear(v.COLOR_BUFFER_BIT)}const S=yield createImageBitmap(p);s.postMessage({id:w,bitmap:S,width:p.width,height:p.height,dataURLOptions:o.dataURLOptions},[S])})),h=requestAnimationFrame(f)};h=requestAnimationFrame(f),this.resetObservers=()=>{a(),cancelAnimationFrame(h)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Qt(t,r,n,!1),o=wn(this.processMutation.bind(this),t,r,n),a=Mn(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>gn(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class kn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Vr,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:vt(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class Tn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Ae()})}let N,Ue,at,He=!1;const J=Ir();function Ee(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:u=null,inlineStylesheet:c=!0,maskAllInputs:h,maskInputOptions:m,slimDOMOptions:f,maskInputFn:g,maskTextFn:p,hooks:y,packFn:w,sampling:S={},dataURLOptions:v={},mousemoveWait:b,recordDOM:I=!0,recordCanvas:B=!1,recordCrossOriginIframes:P=!1,recordAfter:k=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:T=!1,collectFonts:G=!1,inlineImages:V=!1,plugins:O,keepIframeSrcFn:le=()=>!1,ignoreCSSAttributes:z=new Set([]),errorHandler:ne}=e;Qr(ne);const Z=P?window.parent===window:!0;let Ye=!1;if(!Z)try{window.parent.document&&(Ye=!1)}catch{Ye=!0}if(Z&&!t)throw new Error("emit function is required");b!==void 0&&S.mousemove===void 0&&(S.mousemove=b),J.reset();const ht=h===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},pt=f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:f==="all",headMetaDescKeywords:f==="all"}:f||{};Gr();let fr,mt=0;const hr=M=>{for(const K of O||[])K.eventProcessor&&(M=K.eventProcessor(M));return w&&!Ye&&(M=w(M)),M};N=(M,K)=>{var D;if(!((D=oe[0])===null||D===void 0)&&D.isFrozen()&&M.type!==E.FullSnapshot&&!(M.type===E.IncrementalSnapshot&&M.data.source===C.Mutation)&&oe.forEach(q=>q.unfreeze()),Z)t?.(hr(M),K);else if(Ye){const q={type:"rrweb",event:hr(M),origin:window.location.origin,isCheckout:K};window.parent.postMessage(q,"*")}if(M.type===E.FullSnapshot)fr=M,mt=0;else if(M.type===E.IncrementalSnapshot){if(M.data.source===C.Mutation&&M.data.isAttachIframe)return;mt++;const q=n&&mt>=n,de=r&&M.timestamp-fr.timestamp>r;(q||de)&&Ue(!0)}};const Qe=M=>{N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Mutation},M)}))},pr=M=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Scroll},M)})),mr=M=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},M)})),Qn=M=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},M)})),ue=new kn({mutationCb:Qe,adoptedStyleSheetCb:Qn}),ce=new pn({mirror:J,mutationCb:Qe,stylesheetManager:ue,recordCrossOriginIframes:P,wrappedEmit:N});for(const M of O||[])M.getMirror&&M.getMirror({nodeMirror:J,crossOriginIframeMirror:ce.crossOriginIframeMirror,crossOriginIframeStyleMirror:ce.crossOriginIframeStyleMirror});const gt=new Tn;at=new xn({recordCanvas:B,mutationCb:mr,win:window,blockClass:i,blockSelector:o,mirror:J,sampling:S.canvas,dataURLOptions:v});const Ze=new mn({mutationCb:Qe,scrollCb:pr,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:u,inlineStylesheet:c,maskInputOptions:ht,dataURLOptions:v,maskTextFn:p,maskInputFn:g,recordCanvas:B,inlineImages:V,sampling:S,slimDOMOptions:pt,iframeManager:ce,stylesheetManager:ue,canvasManager:at,keepIframeSrcFn:le,processedNodeManager:gt},mirror:J});Ue=(M=!1)=>{if(!I)return;N(L({type:E.Meta,data:{href:window.location.href,width:Tt(),height:kt()}}),M),ue.reset(),Ze.init(),oe.forEach(D=>D.lock());const K=$r(document,{mirror:J,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:u,inlineStylesheet:c,maskAllInputs:ht,maskTextFn:p,slimDOM:pt,dataURLOptions:v,recordCanvas:B,inlineImages:V,onSerialize:D=>{Nt(D,J)&&ce.addIframe(D),At(D,J)&&ue.trackLinkElement(D),ot(D)&&Ze.addShadowRoot(D.shadowRoot,document)},onIframeLoad:(D,q)=>{ce.attachIframe(D,q),Ze.observeAttachShadow(D)},onStylesheetLoad:(D,q)=>{ue.attachLinkElement(D,q)},keepIframeSrcFn:le});if(!K)return console.warn("Failed to snapshot the document");N(L({type:E.FullSnapshot,data:{node:K,initialOffset:xt(window)}}),M),oe.forEach(D=>D.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&ue.adoptStyleSheets(document.adoptedStyleSheets,J.getId(document))};try{const M=[],K=q=>{var de;return _(hn)({mutationCb:Qe,mousemoveCb:(R,yt)=>N(L({type:E.IncrementalSnapshot,data:{source:yt,positions:R}})),mouseInteractionCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},R)})),scrollCb:pr,viewportResizeCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},R)})),inputCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Input},R)})),mediaInteractionCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},R)})),styleSheetRuleCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},R)})),styleDeclarationCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},R)})),canvasMutationCb:mr,fontCb:R=>N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Font},R)})),selectionCb:R=>{N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.Selection},R)}))},customElementCb:R=>{N(L({type:E.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},R)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:u,maskInputOptions:ht,inlineStylesheet:c,sampling:S,recordDOM:I,recordCanvas:B,inlineImages:V,userTriggeredOnInput:T,collectFonts:G,doc:q,maskInputFn:g,maskTextFn:p,keepIframeSrcFn:le,blockSelector:o,slimDOMOptions:pt,dataURLOptions:v,mirror:J,iframeManager:ce,stylesheetManager:ue,shadowDomManager:Ze,processedNodeManager:gt,canvasManager:at,ignoreCSSAttributes:z,plugins:((de=O?.filter(R=>R.observer))===null||de===void 0?void 0:de.map(R=>({observer:R.observer,options:R.options,callback:yt=>N(L({type:E.Plugin,data:{plugin:R.name,payload:yt}}))})))||[]},y)};ce.addLoadListener(q=>{try{M.push(K(q.contentDocument))}catch(de){console.warn(de)}});const D=()=>{Ue(),M.push(K(document)),He=!0};return document.readyState==="interactive"||document.readyState==="complete"?D():(M.push(U("DOMContentLoaded",()=>{N(L({type:E.DomContentLoaded,data:{}})),k==="DOMContentLoaded"&&D()})),M.push(U("load",()=>{N(L({type:E.Load,data:{}})),k==="load"&&D()},window))),()=>{M.forEach(q=>q()),gt.destroy(),He=!1,Zr()}}catch(M){console.warn(M)}}Ee.addCustomEvent=(e,t)=>{if(!He)throw new Error("please add custom event after start recording");N(L({type:E.Custom,data:{tag:e,payload:t}}))},Ee.freezePage=()=>{oe.forEach(e=>e.freeze())},Ee.takeFullSnapshot=e=>{if(!He)throw new Error("please take full snapshot after start recording");Ue(e)},Ee.mirror=J;var er={DEBUG:!0,LIB_VERSION:"2.50.0"},W;if(typeof window>"u"){var tr={hostname:""};W={navigator:{userAgent:""},document:{location:tr,referrer:""},screen:{width:0,height:0},location:tr}}else W=window;var ze=24*60*60*1e3,qe=Array.prototype,Rn=Function.prototype,rr=Object.prototype,se=qe.slice,Oe=rr.toString,$e=rr.hasOwnProperty,F=W.console,xe=W.navigator,j=W.document,je=W.opera,Ge=W.screen,ae=xe.userAgent,lt=Rn.bind,nr=qe.forEach,ir=qe.indexOf,or=qe.map,Dn=Array.isArray,ut={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},X={log:function(){if(!d.isUndefined(F)&&F)try{F.log.apply(F,arguments)}catch{d.each(arguments,function(t){F.log(t)})}},warn:function(){if(!d.isUndefined(F)&&F){var e=["Mixpanel warning:"].concat(d.toArray(arguments));try{F.warn.apply(F,e)}catch{d.each(e,function(r){F.warn(r)})}}},error:function(){if(!d.isUndefined(F)&&F){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{F.error.apply(F,e)}catch{d.each(e,function(r){F.error(r)})}}},critical:function(){if(!d.isUndefined(F)&&F){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{F.error.apply(F,e)}catch{d.each(e,function(r){F.error(r)})}}}},ct=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(X,arguments)}},Ve=function(e){return{log:ct(X.log,e),error:ct(X.error,e),critical:ct(X.critical,e)}};d.bind=function(e,t){var r,n;if(lt&&e.bind===lt)return lt.apply(e,se.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=se.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(se.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(se.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(nr&&e.forEach===nr)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,u="",c="",h=0,m=a,f=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,f=[],Oe.apply(g)==="[object Array]"){for(h=g.length,s=0;s="0"&&t<="9";)g+=t,o();if(t===".")for(g+=".";o()&&t>="0"&&t<="9";)g+=t;if(t==="e"||t==="E")for(g+=t,o(),(t==="-"||t==="+")&&(g+=t,o());t>="0"&&t<="9";)g+=t,o();if(f=+g,!isFinite(f))i("Bad number");else return f},l=function(){var f,g,p="",y;if(t==='"')for(;o();){if(t==='"')return o(),p;if(t==="\\")if(o(),t==="u"){for(y=0,g=0;g<4&&(f=parseInt(o(),16),!!isFinite(f));g+=1)y=y*16+f;p+=String.fromCharCode(y)}else if(typeof r[t]=="string")p+=r[t];else break;else p+=t}i("Bad string")},s=function(){for(;t&&t<=" ";)o()},u=function(){switch(t){case"t":return o("t"),o("r"),o("u"),o("e"),!0;case"f":return o("f"),o("a"),o("l"),o("s"),o("e"),!1;case"n":return o("n"),o("u"),o("l"),o("l"),null}i('Unexpected "'+t+'"')},c,h=function(){var f=[];if(t==="["){if(o("["),s(),t==="]")return o("]"),f;for(;t;){if(f.push(c()),s(),t==="]")return o("]"),f;o(","),s()}}i("Bad array")},m=function(){var f,g={};if(t==="{"){if(o("{"),s(),t==="}")return o("}"),g;for(;t;){if(f=l(),s(),o(":"),Object.hasOwnProperty.call(g,f)&&i('Duplicate key "'+f+'"'),g[f]=c(),s(),t==="}")return o("}"),g;o(","),s()}}i("Bad object")};return c=function(){switch(s(),t){case"{":return m();case"[":return h();case'"':return l();case"-":return a();default:return t>="0"&&t<="9"?a():u()}},function(f){var g;return n=f,e=0,t=" ",g=c(),s(),t&&i("Syntax error"),g}}(),d.base64Encode=function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r,n,i,o,a,l,s,u,c=0,h=0,m="",f=[];if(!e)return e;e=d.utf8Encode(e);do r=e.charCodeAt(c++),n=e.charCodeAt(c++),i=e.charCodeAt(c++),u=r<<16|n<<8|i,o=u>>18&63,a=u>>12&63,l=u>>6&63,s=u&63,f[h++]=t.charAt(o)+t.charAt(a)+t.charAt(l)+t.charAt(s);while(c127&&a<2048?l=String.fromCharCode(a>>6|192,a&63|128):l=String.fromCharCode(a>>12|224,a>>6&63|128,a&63|128),l!==null&&(n>r&&(t+=e.substring(r,n)),t+=l,r=n=o+1)}return n>r&&(t+=e.substring(r,e.length)),t},d.UUID=function(){var e=function(){var n=1*new Date,i;if(W.performance&&W.performance.now)i=W.performance.now();else for(i=0;n==1*new Date;)i++;return n.toString(16)+Math.floor(i).toString(16)},t=function(){return Math.random().toString(16).replace(".","")},r=function(){var n=ae,i,o,a=[],l=0;function s(u,c){var h,m=0;for(h=0;h=4&&(l=s(l,a),a=[]);return a.length>0&&(l=s(l,a)),l.toString(16)};return function(){var n=(Ge.height*Ge.width).toString(16);return e()+"-"+t()+"-"+r()+"-"+n+"-"+e()}}();var sr=["ahrefsbot","ahrefssiteaudit","baiduspider","bingbot","bingpreview","chrome-lighthouse","facebookexternal","petalbot","pinterest","screaming frog","yahoo! slurp","yandexbot","adsbot-google","apis-google","duplexweb-google","feedfetcher-google","google favicon","google web preview","google-read-aloud","googlebot","googleweblight","mediapartners-google","storebot-google"];d.isBlockedUA=function(e){var t;for(e=e.toLowerCase(),t=0;t=0}function n(i){if(!j.getElementsByTagName)return[];var o=i.split(" "),a,l,s,u,c,h,m,f,g,p,y=[j];for(h=0;h-1){l=a.split("#"),s=l[0];var w=l[1],S=j.getElementById(w);if(!S||s&&S.nodeName.toLowerCase()!=s)return[];y=[S];continue}if(a.indexOf(".")>-1){l=a.split("."),s=l[0];var v=l[1];for(s||(s="*"),u=[],c=0,m=0;m-1};break;default:k=function(T){return T.getAttribute(I)}}for(y=[],p=0,m=0;m=3?t[2]:""},currentUrl:function(){return W.location.href},properties:function(e){return typeof e!="object"&&(e={}),d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ae,xe.vendor,je),$referrer:j.referrer,$referring_domain:d.info.referringDomain(j.referrer),$device:d.info.device(ae)}),{$current_url:d.info.currentUrl(),$browser_version:d.info.browserVersion(ae,xe.vendor,je),$screen_height:Ge.height,$screen_width:Ge.width,mp_lib:"web",$lib_version:er.LIB_VERSION,$insert_id:ft(),time:d.timestamp()/1e3},d.strip_empty_properties(e))},people_properties:function(){return d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ae,xe.vendor,je)}),{$browser_version:d.info.browserVersion(ae,xe.vendor,je)})},mpPageViewProperties:function(){return d.strip_empty_properties({current_page_title:j.title,current_domain:W.location.hostname,current_url_path:W.location.pathname,current_url_protocol:W.location.protocol,current_url_search:W.location.search})}};var ft=function(e){var t=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return e?t.substring(0,e):t},Fn=function(e){var t=new XMLHttpRequest;if(t.open(e.method,e.url,!0),d.each(e.headers,function(n,i){t.setRequestHeader(i,n)}),e.timeout_ms&&typeof t.timeout<"u"){t.timeout=e.timeout_ms;var r=new Date().getTime()}t.withCredentials=!0,t.onreadystatechange=function(){if(t.readyState===4)if(t.status===200){if(e.callback)if(e.verbose){var n;try{n=d.JSONDecode(t.responseText)}catch(o){if(e.report_error(o),e.ignore_json_errors)n=t.responseText;else return}e.callback(n)}else e.callback(Number(t.responseText))}else{var i;t.timeout&&!t.status&&new Date().getTime()-r>=t.timeout?i="timeout":i="Bad HTTP status: "+t.status+" "+t.statusText,e.report_error(i),e.callback&&(e.verbose?e.callback({status:0,error:i,xhr_req:t}):e.callback(0))}},t.send(e.body_data)},Pn=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Bn=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,ar=function(e){var t=Bn,r=e.split("."),n=r[r.length-1];(n.length>4||n==="com"||n==="org")&&(t=Pn);var i=e.match(t);return i?i[0]:""},Xe=null,Ke=null;typeof JSON<"u"&&(Xe=JSON.stringify,Ke=JSON.parse),Xe=Xe||d.JSONEncode,Ke=Ke||d.JSONDecode,d.toArray=d.toArray,d.isObject=d.isObject,d.JSONEncode=d.JSONEncode,d.JSONDecode=d.JSONDecode,d.isBlockedUA=d.isBlockedUA,d.isEmptyObject=d.isEmptyObject,d.info=d.info,d.info.device=d.info.device,d.info.browser=d.info.browser,d.info.browserVersion=d.info.browserVersion,d.info.properties=d.info.properties;var Wn="__mp_opt_in_out_";function Un(e,t){if(jn(t))return X.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),!0;var r=$n(e,t)==="0";return r&&X.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."),r}function Hn(e){return Gn(e,function(t){return this.get_config(t)})}function zn(e){return e=e||{},e.persistenceType==="localStorage"?d.localStorage:d.cookie}function qn(e,t){return t=t||{},(t.persistencePrefix||Wn)+e}function $n(e,t){return zn(t).get(qn(e,t))}function jn(e){if(e&&e.ignoreDnt)return!1;var t=e&&e.window||W,r=t.navigator||{},n=!1;return d.each([r.doNotTrack,r.msDoNotTrack,t.doNotTrack],function(i){d.includes([!0,1,"1","yes"],i)&&(n=!0)}),n}function Gn(e,t){return function(){var r=!1;try{var n=t.call(this,"token"),i=t.call(this,"ignore_dnt"),o=t.call(this,"opt_out_tracking_persistence_type"),a=t.call(this,"opt_out_tracking_cookie_prefix"),l=t.call(this,"window");n&&(r=Un(n,{ignoreDnt:i,persistenceType:o,persistencePrefix:a,window:l}))}catch(u){X.error("Unexpected error when checking tracking opt-out status: "+u)}if(!r)return e.apply(this,arguments);var s=arguments[arguments.length-1];typeof s=="function"&&s(0)}}var Vn=Ve("lock"),lr=function(e,t){t=t||{},this.storageKey=e,this.storage=t.storage||window.localStorage,this.pollIntervalMS=t.pollIntervalMS||100,this.timeoutMS=t.timeoutMS||2e3};lr.prototype.withLock=function(e,t,r){!r&&typeof t!="function"&&(r=t,t=null);var n=r||new Date().getTime()+"|"+Math.random(),i=new Date().getTime(),o=this.storageKey,a=this.pollIntervalMS,l=this.timeoutMS,s=this.storage,u=o+":X",c=o+":Y",h=o+":Z",m=function(S){t&&t(S)},f=function(S){if(new Date().getTime()-i>l){Vn.error("Timeout waiting for mutex on "+o+"; clearing lock. ["+n+"]"),s.removeItem(h),s.removeItem(c),y();return}setTimeout(function(){try{S()}catch(v){m(v)}},a*(Math.random()+.1))},g=function(S,v){S()?v():f(function(){g(S,v)})},p=function(){var S=s.getItem(c);if(S&&S!==n)return!1;if(s.setItem(c,n),s.getItem(c)===n)return!0;if(!Je(s,!0))throw new Error("localStorage support dropped while acquiring lock");return!1},y=function(){s.setItem(u,n),g(p,function(){if(s.getItem(u)===n){w();return}f(function(){if(s.getItem(c)!==n){y();return}g(function(){return!s.getItem(h)},w)})})},w=function(){s.setItem(h,"1");try{e()}finally{s.removeItem(h),s.getItem(c)===n&&s.removeItem(c),s.getItem(u)===n&&s.removeItem(u)}};try{if(Je(s,!0))y();else throw new Error("localStorage support check failed")}catch(S){m(S)}};var ur=Ve("batch"),re=function(e,t){t=t||{},this.storageKey=e,this.storage=t.storage||window.localStorage,this.reportError=t.errorReporter||d.bind(ur.error,ur),this.lock=new lr(e,{storage:this.storage}),this.usePersistence=t.usePersistence,this.pid=t.pid||null,this.memQueue=[]};re.prototype.enqueue=function(e,t,r){var n={id:ft(),flushAfter:new Date().getTime()+t*2,payload:e};if(!this.usePersistence){this.memQueue.push(n),r&&r(!0);return}this.lock.withLock(d.bind(function(){var o;try{var a=this.readFromStorage();a.push(n),o=this.saveToStorage(a),o&&this.memQueue.push(n)}catch{this.reportError("Error enqueueing item",e),o=!1}r&&r(o)},this),d.bind(function(o){this.reportError("Error acquiring storage lock",o),r&&r(!1)},this),this.pid)},re.prototype.fillBatch=function(e){var t=this.memQueue.slice(0,e);if(this.usePersistence&&t.lengtho.flushAfter&&!n[o.id]&&(o.orphaned=!0,t.push(o),t.length>=e))break}}}return t};var cr=function(e,t){var r=[];return d.each(e,function(n){n.id&&!t[n.id]&&r.push(n)}),r};re.prototype.removeItemsByID=function(e,t){var r={};if(d.each(e,function(i){r[i]=!0}),this.memQueue=cr(this.memQueue,r),!this.usePersistence){t&&t(!0);return}var n=d.bind(function(){var i;try{var o=this.readFromStorage();if(o=cr(o,r),i=this.saveToStorage(o),i){o=this.readFromStorage();for(var a=0;a5&&(this.reportError("[dupe] item ID sent too many times, not sending",{item:u,batchSize:i.length,timesSent:this.itemIdsSentSuccessfully[m]}),h=!1):this.reportError("[dupe] found item with no ID",{item:u}),h&&o.push(c)}a[u.id]=c},this),o.length<1){this.resetFlush();return}this.requestInProgress=!0;var l=d.bind(function(u){this.requestInProgress=!1;try{var c=!1;if(e.unloading)this.queue.updatePayloads(a);else if(d.isObject(u)&&u.error==="timeout"&&new Date().getTime()-r>=t)this.reportError("Network timeout; retrying"),this.flush();else if(d.isObject(u)&&u.xhr_req&&(u.xhr_req.status>=500||u.xhr_req.status===429||u.error==="timeout")){var h=this.flushInterval*2,m=u.xhr_req.responseHeaders;if(m){var f=m["Retry-After"];f&&(h=parseInt(f,10)*1e3||h)}h=Math.min(Jn,h),this.reportError("Error; retry in "+h+" ms"),this.scheduleFlush(h)}else if(d.isObject(u)&&u.xhr_req&&u.xhr_req.status===413)if(i.length>1){var g=Math.max(1,Math.floor(n/2));this.batchSize=Math.min(this.batchSize,g,i.length-1),this.reportError("413 response; reducing batch size to "+this.batchSize),this.resetFlush()}else this.reportError("Single-event request too large; dropping",i),this.resetBatchSize(),c=!0;else c=!0;c&&(this.queue.removeItemsByID(d.map(i,function(p){return p.id}),d.bind(function(p){p?(this.consecutiveRemovalFailures=0,this.forceDelayFlush?this.resetFlush():this.flush()):(this.reportError("Failed to remove items from queue"),++this.consecutiveRemovalFailures>5?(this.reportError("Too many queue failures; disabling batching system."),this.options.stopAllBatchingFunc()):this.resetFlush())},this)),d.each(i,d.bind(function(p){var y=p.id;y?(this.itemIdsSentSuccessfully[y]=this.itemIdsSentSuccessfully[y]||0,this.itemIdsSentSuccessfully[y]++,this.itemIdsSentSuccessfully[y]>5&&this.reportError("[dupe] item ID sent too many times",{item:p,batchSize:i.length,timesSent:this.itemIdsSentSuccessfully[y]})):this.reportError("[dupe] found item with no ID while removing",{item:p})},this)))}catch(p){this.reportError("Error handling API response",p),this.resetFlush()}},this),s={method:"POST",verbose:!0,ignore_json_errors:!0,timeout_ms:t};e.unloading&&(s.transport="sendBeacon"),ke.log("MIXPANEL REQUEST:",o),this.options.sendRequestFunc(o,s,l)}catch(u){this.reportError("Error flushing request queue",u),this.resetFlush()}},Y.prototype.reportError=function(e,t){if(ke.error.apply(ke.error,arguments),this.options.errorReporter)try{t instanceof Error||(t=new Error(e)),this.options.errorReporter(e,t)}catch(r){ke.error(r)}};var Se=Ve("recorder"),Xn=1e3,Kn=10*1e3,Yn=90*1e3,Q=function(e){this._mixpanel=e,this._stopRecording=null,this.recEvents=[],this.seqNo=0,this.replayId=null,this.replayStartTime=null,this.batchStartTime=null,this.replayLengthMs=0,this.sendBatchId=null,this.idleTimeoutId=null,this.maxTimeoutId=null,this.recordMaxMs=ze,this._initBatcher()};Q.prototype._initBatcher=function(){this.batcher=new Y("__mprec",{batchSize:Xn,flushIntervalMs:Kn,requestTimeoutMs:Yn,autoStart:!0,sendRequestFunc:d.bind(function(e,t,r){this.sendRequestWithOptOut(e,t,r)},this),forceDelayFlush:!0})},Q.prototype.get_config=function(e){return this._mixpanel.get_config(e)},Q.prototype.startRecording=function(){if(this._stopRecording!==null){Se.log("Recording already in progress, skipping startRecording.");return}this.recordMaxMs=this.get_config("record_max_ms"),this.recordMaxMs>ze&&(this.recordMaxMs=ze,Se.critical("record_max_ms cannot be greater than "+ze+"ms. Capping value.")),this.recEvents=[],this.seqNo=0,this.startDate=new Date,this.replayStartTime=this.startDate.getTime(),this.batchStartTime=this.replayStartTime,this.replayId=d.UUID(),this.replayLengthMs=0,this.batcher.start();var e=d.bind(function(){clearTimeout(this.idleTimeoutId),this.idleTimeoutId=setTimeout(d.bind(function(){Se.log("Idle timeout reached, restarting recording."),this.resetRecording()},this),this.get_config("record_idle_timeout_ms"))},this);this._stopRecording=Ee({emit:d.bind(function(t){this.batcher.enqueue(t),this.replayLengthMs=new Date().getTime()-this.replayStartTime,e()},this),maskAllInputs:!0,maskTextSelector:this.get_config("record_mask_text_selector")}),e(),this.maxTimeoutId=setTimeout(d.bind(this.resetRecording,this),this.recordMaxMs)},Q.prototype.resetRecording=function(){this.stopRecording(),this.startRecording()},Q.prototype.stopRecording=function(){this._stopRecording!==null&&(this._stopRecording(),this._stopRecording=null),this.batcher.flush(),this.replayId=null,clearTimeout(this.idleTimeoutId),clearTimeout(this.maxTimeoutId)},Q.prototype.sendRequestWithOptOut=function(e,t,r){this._sendRequest(e,t,r,d.bind(this._onOptOut,this))},Q.prototype._onOptOut=function(e){e===0&&(this.recEvents=[],this.stopRecording())},Q.prototype._sendRequest=Hn(function(e,t,r){var n=this.get_config("api_host")+"/"+this.get_config("api_routes").record,i={Authorization:"Basic "+btoa(this.get_config("token")+":"),"Content-Type":"application/json"},o={distinct_id:String(this._mixpanel.get_distinct_id()),events:e,seq:this.seqNo++,batch_start_time:this.batchStartTime/1e3,replay_id:this.replayId,replay_length_ms:this.replayLengthMs,replay_start_time:this.replayStartTime/1e3},a=this._mixpanel.get_property("$device_id");a&&(o.$device_id=a);var l=this._mixpanel.get_property("$user_id");l&&(o.$user_id=l);var s=d.JSONEncode(o),u=d.extend({},t,{method:"POST",url:n,body_data:s,headers:i,callback:r,reportError:d.bind(this.reportError,this)});Fn(u)}),Q.prototype.reportError=function(e,t){Se.error.apply(Se.error,arguments);try{!t&&!(e instanceof Error)&&(e=new Error(e)),this.get_config("error_reporter")(e,t)}catch(r){Se.error(r)}},window.__mp_recorder=Q})(); ->>>>>>> 571d8b5 (cleanup, draft) diff --git a/dist/mixpanel.amd.js b/dist/mixpanel.amd.js index e0072480..f00fce89 100644 --- a/dist/mixpanel.amd.js +++ b/dist/mixpanel.amd.js @@ -1,13 +1,8 @@ define((function () { 'use strict'; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1685,83 +1680,6 @@ define((function () { 'use strict'; return maxlen ? guid.substring(0, maxlen) : guid; }; - - /** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ - var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); - }; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2133,7 +2051,6 @@ define((function () { 'use strict'; this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2158,14 +2075,6 @@ define((function () { 'use strict'; 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2191,7 +2100,6 @@ define((function () { 'use strict'; }, this), this.pid); }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2200,7 +2108,7 @@ define((function () { 'use strict'; */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2249,13 +2157,7 @@ define((function () { 'use strict'; _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2337,13 +2239,6 @@ define((function () { 'use strict'; */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2405,10 +2300,7 @@ define((function () { 'use strict'; */ RequestQueue.prototype.clear = function() { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; // maximum interval between request retries after exponential backoff @@ -2423,23 +2315,22 @@ define((function () { 'use strict'; * @constructor */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2450,7 +2341,7 @@ define((function () { 'use strict'; * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -2482,26 +2373,26 @@ define((function () { 'use strict'; }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -2524,16 +2415,16 @@ define((function () { 'use strict'; } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2634,16 +2525,12 @@ define((function () { 'use strict'; _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -2685,7 +2572,8 @@ define((function () { 'use strict'; requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2697,12 +2585,12 @@ define((function () { 'use strict'; */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4798,22 +4686,69 @@ define((function () { 'use strict'; } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -4904,10 +4839,7 @@ define((function () { 'use strict'; return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/dist/mixpanel.cjs.js b/dist/mixpanel.cjs.js index a99b854a..037d365e 100644 --- a/dist/mixpanel.cjs.js +++ b/dist/mixpanel.cjs.js @@ -1,13 +1,8 @@ 'use strict'; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1685,83 +1680,6 @@ var cheap_guid = function(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; - -/** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ -var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); -}; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2133,7 +2051,6 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2158,14 +2075,6 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2191,7 +2100,6 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { }, this), this.pid); }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2200,7 +2108,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2249,13 +2157,7 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2337,13 +2239,6 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2405,10 +2300,7 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; // maximum interval between request retries after exponential backoff @@ -2423,23 +2315,22 @@ var logger = console_with_prefix('batch'); * @constructor */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2450,7 +2341,7 @@ var RequestBatcher = function(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -2482,26 +2373,26 @@ RequestBatcher.prototype.clear = function() { }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -2524,16 +2415,16 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2634,16 +2525,12 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -2685,7 +2572,8 @@ RequestBatcher.prototype.flush = function(options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2697,12 +2585,12 @@ RequestBatcher.prototype.flush = function(options) { */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4798,22 +4686,69 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -4904,10 +4839,7 @@ MixpanelLib.prototype.init_batchers = function() { return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/dist/mixpanel.globals.js b/dist/mixpanel.globals.js index 3f2b0f39..979211bf 100644 --- a/dist/mixpanel.globals.js +++ b/dist/mixpanel.globals.js @@ -2,13 +2,8 @@ 'use strict'; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1686,83 +1681,6 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; - - /** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ - var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); - }; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2134,7 +2052,6 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2159,14 +2076,6 @@ 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2192,7 +2101,6 @@ }, this), this.pid); }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2201,7 +2109,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2250,13 +2158,7 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2338,13 +2240,6 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2406,10 +2301,7 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; // maximum interval between request retries after exponential backoff @@ -2424,23 +2316,22 @@ * @constructor */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2451,7 +2342,7 @@ * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -2483,26 +2374,26 @@ }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -2525,16 +2416,16 @@ } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2635,16 +2526,12 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -2686,7 +2573,8 @@ requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2698,12 +2586,12 @@ */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4799,22 +4687,69 @@ } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -4905,10 +4840,7 @@ return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/dist/mixpanel.min.js b/dist/mixpanel.min.js index 2257c868..bf19d8f6 100644 --- a/dist/mixpanel.min.js +++ b/dist/mixpanel.min.js @@ -1,5 +1,4 @@ (function() { -<<<<<<< HEAD var l=void 0,m=!0,r=null,D=!1; (function(){function Aa(){function a(){if(!a.Dc)la=a.Dc=m,ma=D,c.a(F,function(a){a.qc()})}function b(){try{t.documentElement.doScroll("left")}catch(d){setTimeout(b,1);return}a()}if(t.addEventListener)"complete"===t.readyState?a():t.addEventListener("DOMContentLoaded",a,D);else if(t.attachEvent){t.attachEvent("onreadystatechange",a);var d=D;try{d=n.frameElement===r}catch(f){}t.documentElement.doScroll&&d&&b()}c.Tb(n,"load",a,m)}function Ba(){x.init=function(a,b,d){if(d)return x[d]||(x[d]=F[d]=S(a, b,d),x[d].ka()),x[d];d=x;if(F.mixpanel)d=F.mixpanel;else if(a)d=S(a,b,"mixpanel"),d.ka(),F.mixpanel=d;x=d;1===ca&&(n.mixpanel=x);Ca()}}function Ca(){c.a(F,function(a,b){"mixpanel"!==b&&(x[b]=a)});x._=c}function da(a){a=c.e(a)?a:c.g(a)?{}:{days:a};return c.extend({},Da,a)}function S(a,b,d){var f,h="mixpanel"===d?x:x[d];if(h&&0===ca)f=h;else{if(h&&!c.isArray(h)){o.error("You have already initialized "+d);return}f=new e}f.kb={};f.X(a,b,d);f.people=new j;f.people.X(f);if(!f.c("skip_first_touch_marketing")){var a= @@ -110,116 +109,4 @@ e.prototype.s;e.prototype.get_distinct_id=e.prototype.L;e.prototype.toString=e.p e.prototype.ud;e.prototype.start_batch_senders=e.prototype.ab;e.prototype.stop_batch_senders=e.prototype.bb;e.prototype.start_session_recording=e.prototype.dc;e.prototype.stop_session_recording=e.prototype.ld;e.prototype.get_session_recording_properties=e.prototype.Db;e.prototype.DEFAULT_API_ROUTES=A;q.prototype.properties=q.prototype.aa;q.prototype.update_search_keyword=q.prototype.lc;q.prototype.update_referrer_info=q.prototype.gb;q.prototype.get_cross_subdomain=q.prototype.Gc;q.prototype.clear= q.prototype.clear;var F={};(function(){ca=1;x=n.mixpanel;c.g(x)?o.A('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.'):x.__loaded||x.config&&x.persistence?o.A("The Mixpanel library has already been downloaded at least once. Ensure that the Mixpanel code snippet only appears once on the page (and is not double-loaded by a tag manager) in order to avoid errors."):1.1>(x.__SV||0)?o.A("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."): (c.a(x._i,function(a){a&&c.isArray(a)&&(F[a[a.length-1]]=S.apply(this,a))}),Ba(),x.init(),c.a(F,function(a){a.ka()}),Aa())})()})(); -======= -var l=void 0,m=!0,q=null,D=!1; -(function(){function Aa(){function a(){if(!a.Jc)la=a.Jc=m,ma=D,c.c(F,function(a){a.uc()})}function b(){try{t.documentElement.doScroll("left")}catch(d){setTimeout(b,1);return}a()}if(t.addEventListener)"complete"===t.readyState?a():t.addEventListener("DOMContentLoaded",a,D);else if(t.attachEvent){t.attachEvent("onreadystatechange",a);var d=D;try{d=j.frameElement===q}catch(f){}t.documentElement.doScroll&&d&&b()}c.Zb(j,"load",a,m)}function Ba(){x.init=function(a,b,d){if(d)return x[d]||(x[d]=F[d]=S(a, -b,d),x[d].ka()),x[d];d=x;if(F.mixpanel)d=F.mixpanel;else if(a)d=S(a,b,"mixpanel"),d.ka(),F.mixpanel=d;x=d;1===ca&&(j.mixpanel=x);Ca()}}function Ca(){c.c(F,function(a,b){"mixpanel"!==b&&(x[b]=a)});x._=c}function da(a){a=c.e(a)?a:c.g(a)?{}:{days:a};return c.extend({},Da,a)}function S(a,b,d){var f,g="mixpanel"===d?x:x[d];if(g&&0===ca)f=g;else{if(g&&!c.isArray(g)){p.error("You have already initialized "+d);return}f=new e}f.nb={};f.Y(a,b,d);f.people=new n;f.people.Y(f);if(!f.a("skip_first_touch_marketing")){var a= -c.info.Z(q),h={},u=D;c.c(a,function(a,b){(h["initial_"+b]=a)&&(u=m)});u&&f.people.N(h)}J=J||f.a("debug");!c.g(g)&&c.isArray(g)&&(f.Ba.call(f.people,g.people),f.Ba(g));return f}function e(){}function P(){}function Ea(a){return a}function o(a){this.props={};this.Ed=D;this.name=a.persistence_name?"mp_"+a.persistence_name:"mp_"+a.token+"_mixpanel";var b=a.persistence;if("cookie"!==b&&"localStorage"!==b)p.A("Unknown persistence type "+b+"; falling back to cookie"),b=a.persistence="cookie";this.i="localStorage"=== -b&&c.localStorage.ra()?c.localStorage:c.cookie;this.load();this.pc(a);this.Ad(a);this.save()}function n(){}function v(){}function C(a,b){this.options=b;this.ca=new G(a,{oa:c.bind(this.h,this),i:b.i,C:b.C});this.Db=this.options.F;this.Ma=this.options.Hb;this.Ib=b.Ib||D;this.ua=!this.options.Cc;this.Ka=0;this.H={}}function na(a,b){var d=[];c.c(a,function(a){var c=a.id;if(c in b){if(c=b[c],c!==q)a.payload=c,d.push(a)}else d.push(a)});return d}function oa(a,b){var d=[];c.c(a,function(a){a.id&&!b[a.id]&& -d.push(a)});return d}function G(a,b){b=b||{};this.O=a;this.i=b.i||window.localStorage;this.h=b.oa||c.bind(pa.error,pa);this.$a=new qa(a,{i:this.i});this.C=b.C;this.ta=b.ta||q;this.B=[]}function qa(a,b){b=b||{};this.O=a;this.i=b.i||window.localStorage;this.Xb=b.Xb||100;this.kc=b.kc||2E3}function T(){this.Ub="submit"}function M(){this.Ub="click"}function E(){}function ra(a){var b=Fa,d=a.split("."),d=d[d.length-1];if(4=b.timeout?"timeout":"Bad HTTP status: "+ -b.status+" "+b.statusText,a.l(f),a.J&&(a.xa?a.J({status:0,error:f,S:b}):a.J(0))};b.send(a.Ec)}function ea(a){var b=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return a?b.substring(0,a):b}function U(a,b){if(fa!==q&&!b)return fa;var d=m;try{var a=a||window.localStorage,c="__mplss_"+ea(8);a.setItem(c,"xyz");"xyz"!==a.getItem(c)&&(d=D);a.removeItem(c)}catch(g){d=D}return fa=d}function ga(a){return{log:ha(p.log,a),error:ha(p.error,a),A:ha(p.A,a)}}function ha(a, -b){return function(){arguments[0]="["+b+"] "+arguments[0];return a.apply(p,arguments)}}function Ia(a,b){sa(m,a,b)}function Ja(a,b){sa(D,a,b)}function Ka(a,b){return"1"===V(b).get(W(a,b))}function ta(a,b){if(La(b))return p.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),m;var d="0"===V(b).get(W(a,b));d&&p.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."); -return d}function K(a){return ia(a,function(a){return this.a(a)})}function H(a){return ia(a,function(a){return this.p(a)})}function N(a){return ia(a,function(a){return this.p(a)})}function Ma(a,b){b=b||{};V(b).remove(W(a,b),!!b.Bb,b.zb)}function V(a){a=a||{};return"localStorage"===a.Wb?c.localStorage:c.cookie}function W(a,b){b=b||{};return(b.Vb||Na)+a}function La(a){if(a&&a.Lb)return D;var a=a&&a.window||j,b=a.navigator||{},d=D;c.c([b.doNotTrack,b.msDoNotTrack,a.doNotTrack],function(a){c.j([m,1,"1", -"yes"],a)&&(d=m)});return d}function sa(a,b,d){!c.Za(b)||!b.length?p.error("gdpr."+(a?"optIn":"optOut")+" called with an invalid token"):(d=d||{},V(d).set(W(b,d),a?1:0,c.Pb(d.Ab)?d.Ab:q,!!d.Bb,!!d.dd,!!d.Gc,d.zb),d.o&&a&&d.o(d.td||"$opt_in",d.ud,{send_immediately:m}))}function ia(a,b){return function(){var d=D;try{var c=b.call(this,"token"),g=b.call(this,"ignore_dnt"),h=b.call(this,"opt_out_tracking_persistence_type"),u=b.call(this,"opt_out_tracking_cookie_prefix"),i=b.call(this,"window");c&&(d=ta(c, -{Lb:g,Wb:h,Vb:u,window:i}))}catch(e){p.error("Unexpected error when checking tracking opt-out status: "+e)}if(!d)return a.apply(this,arguments);d=arguments[arguments.length-1];"function"===typeof d&&d(0)}}var J=m,j;if("undefined"===typeof window){var A={hostname:""};j={navigator:{userAgent:""},document:{location:A,referrer:""},screen:{width:0,height:0},location:A}}else j=window;var A=Array.prototype,ua=Object.prototype,L=A.slice,Q=ua.toString,X=ua.hasOwnProperty,y=j.console,I=j.navigator,t=j.document, -Y=j.opera,Z=j.screen,z=I.userAgent,ja=Function.prototype.bind,va=A.forEach,wa=A.indexOf,xa=A.map,A=Array.isArray,ka={},c={trim:function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},p={log:function(){if(J&&!c.g(y)&&y)try{y.log.apply(y,arguments)}catch(a){c.c(arguments,function(a){y.log(a)})}},warn:function(){if(J&&!c.g(y)&&y){var a=["Mixpanel warning:"].concat(c.P(arguments));try{y.warn.apply(y,a)}catch(b){c.c(a,function(a){y.warn(a)})}}},error:function(){if(J&&!c.g(y)&&y){var a= -["Mixpanel error:"].concat(c.P(arguments));try{y.error.apply(y,a)}catch(b){c.c(a,function(a){y.error(a)})}}},A:function(){if(!c.g(y)&&y){var a=["Mixpanel error:"].concat(c.P(arguments));try{y.error.apply(y,a)}catch(b){c.c(a,function(a){y.error(a)})}}}};c.bind=function(a,b){var d,f;if(ja&&a.bind===ja)return ja.apply(a,L.call(arguments,1));if(!c.Ya(a))throw new TypeError;d=L.call(arguments,2);return f=function(){if(!(this instanceof f))return a.apply(b,d.concat(L.call(arguments)));var c={};c.prototype= -a.prototype;var h=new c;c.prototype=q;c=a.apply(h,d.concat(L.call(arguments)));return Object(c)===c?c:h}};c.c=function(a,b,d){if(!(a===q||a===l))if(va&&a.forEach===va)a.forEach(b,d);else if(a.length===+a.length)for(var c=0,g=a.length;ca?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())};c.fa=function(a){var b= -{};c.c(a,function(a,f){c.Za(a)&&0=i;)g()}function d(){var a,b,d="",c;if('"'===i)for(;g();){if('"'===i)return g(),d;if("\\"===i)if(g(),"u"===i){for(b=c=0;4>b;b+=1){a=parseInt(g(),16);if(!isFinite(a))break;c=16*c+a}d+=String.fromCharCode(c)}else if("string"===typeof k[i])d+=k[i];else break;else d+=i}h("Bad string")}function c(){var a;a="";"-"===i&&(a="-",g("-"));for(;"0"<=i&&"9">=i;)a+=i,g();if("."===i)for(a+=".";g()&&"0"<=i&&"9">=i;)a+=i;if("e"===i||"E"===i){a+=i;g();if("-"===i||"+"===i)a+=i,g();for(;"0"<=i&&"9">=i;)a+=i,g()}a= -+a;if(isFinite(a))return a;h("Bad number")}function g(a){a&&a!==i&&h("Expected '"+a+"' instead of '"+i+"'");i=s.charAt(e);e+=1;return i}function h(a){a=new SyntaxError(a);a.Dd=e;a.text=s;throw a;}var e,i,k={'"':'"',"\\":"\\","/":"/",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"},s,r;r=function(){b();switch(i){case "{":var e;a:{var u,k={};if("{"===i){g("{");b();if("}"===i){g("}");e=k;break a}for(;i;){u=d();b();g(":");Object.hasOwnProperty.call(k,u)&&h('Duplicate key "'+u+'"');k[u]=r();b();if("}"===i){g("}"); -e=k;break a}g(",");b()}}h("Bad object")}return e;case "[":a:{e=[];if("["===i){g("[");b();if("]"===i){g("]");u=e;break a}for(;i;){e.push(r());b();if("]"===i){g("]");u=e;break a}g(",");b()}}h("Bad array")}return u;case '"':return d();case "-":return c();default:return"0"<=i&&"9">=i?c():a()}};return function(a){s=a;e=0;i=" ";a=r();b();i&&h("Syntax error");return a}}();c.Dc=function(a){var b,d,f,g,h=0,e=0,i="",i=[];if(!a)return a;a=c.Bd(a);do b=a.charCodeAt(h++),d=a.charCodeAt(h++),f=a.charCodeAt(h++), -g=b<<16|d<<8|f,b=g>>18&63,d=g>>12&63,f=g>>6&63,g&=63,i[e++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(d)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(g);while(he?c++:i=127e?String.fromCharCode(e>>6|192,e&63|128):String.fromCharCode(e>>12|224,e>>6&63|128,e&63|128);i!==q&&(c>d&&(b+=a.substring(d,c)),b+=i,d=c=h+1)}c>d&&(b+=a.substring(d,a.length));return b};c.lb=function(){function a(){function a(b,d){var c,f=0;for(c=0;cn?(Ra.error("Timeout waiting for mutex on "+r+"; clearing lock. ["+k+"]"),j.removeItem(t),j.removeItem(p),g()):setTimeout(function(){try{a()}catch(c){b&&b(c)}},w*(Math.random()+0.1))}!c&&"function"!==typeof b&&(c=b,b=q);var k=c|| -(new Date).getTime()+"|"+Math.random(),s=(new Date).getTime(),r=this.O,w=this.Xb,n=this.kc,j=this.i,o=r+":X",p=r+":Y",t=r+":Z";try{if(U(j,m))g();else throw Error("localStorage support check failed");}catch(v){b&&b(v)}};var pa=ga("batch");G.prototype.Pa=function(a,b,d){var f={id:ea(),flushAfter:(new Date).getTime()+2*b,payload:a};this.C?this.$a.kb(c.bind(function(){var b;try{var c=this.ea();c.push(f);(b=this.cb(c))&&this.B.push(f)}catch(e){this.h("Error enqueueing item",a),b=D}d&&d(b)},this),c.bind(function(a){this.h("Error acquiring storage lock", -a);d&&d(D)},this),this.ta):(this.B.push(f),d&&d(m))};G.prototype.Kc=function(a){var b=this.B.slice(0,a);if(this.C&&b.lengthh.flushAfter&&!f[h.id]&&(h.Wc=m,b.push(h),b.length>=a))break}}}return b};G.prototype.Yc=function(a,b){var d={};c.c(a,function(a){d[a]=m});this.B=oa(this.B,d);if(this.C){var f=c.bind(function(){var b;try{var c=this.ea(),c=oa(c,d);if(b=this.cb(c))for(var c= -this.ea(),f=0;fe.length)this.M();else{this.ac=m;var i=c.bind(function(e){this.ac=D;try{var h=D;if(a.nc)this.ca.zd(u);else if(c.e(e)&&"timeout"===e.error&&(new Date).getTime()-d>=b)this.h("Network timeout; retrying"),this.flush();else if(c.e(e)&&e.S&&(500<=e.S.status||429===e.S.status||"timeout"===e.error)){var i=2*this.Gd,k=e.S.responseHeaders;if(k){var j=k["Retry-After"];j&&(i=1E3*parseInt(j,10)||i)}i=Math.min(6E5,i);this.h("Error; retry in "+ -i+" ms");this.cc(i)}else if(c.e(e)&&e.S&&413===e.S.status)if(1(x.__SV||0)?p.A("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."):(c.c(x._i,function(a){a&&c.isArray(a)&&(F[a[a.length-1]]=S.apply(this,a))}),Ba(),x.init(),c.c(F,function(a){a.ka()}),Aa())})()})(); ->>>>>>> 571d8b5 (cleanup, draft) })(); diff --git a/dist/mixpanel.umd.js b/dist/mixpanel.umd.js index 7f6083ef..f8a611b1 100644 --- a/dist/mixpanel.umd.js +++ b/dist/mixpanel.umd.js @@ -5,13 +5,8 @@ })(this, (function () { 'use strict'; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1689,83 +1684,6 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; - - /** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ - var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); - }; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2137,7 +2055,6 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2162,14 +2079,6 @@ 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2195,7 +2104,6 @@ }, this), this.pid); }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2204,7 +2112,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2253,13 +2161,7 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2341,13 +2243,6 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2409,10 +2304,7 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; // maximum interval between request retries after exponential backoff @@ -2427,23 +2319,22 @@ * @constructor */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2454,7 +2345,7 @@ * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -2486,26 +2377,26 @@ }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -2528,16 +2419,16 @@ } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2638,16 +2529,12 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -2689,7 +2576,8 @@ requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2701,12 +2589,12 @@ */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4802,22 +4690,69 @@ } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -4908,10 +4843,7 @@ return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/examples/commonjs-browserify/bundle.js b/examples/commonjs-browserify/bundle.js index 927c65ea..1ce5fc3f 100644 --- a/examples/commonjs-browserify/bundle.js +++ b/examples/commonjs-browserify/bundle.js @@ -2,13 +2,8 @@ 'use strict'; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1686,83 +1681,6 @@ var cheap_guid = function(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; - -/** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ -var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); -}; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2134,7 +2052,6 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2159,14 +2076,6 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2192,7 +2101,6 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { }, this), this.pid); }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2201,7 +2109,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2250,13 +2158,7 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2338,13 +2240,6 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2406,10 +2301,7 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; // maximum interval between request retries after exponential backoff @@ -2424,23 +2316,22 @@ var logger = console_with_prefix('batch'); * @constructor */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2451,7 +2342,7 @@ var RequestBatcher = function(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -2483,26 +2374,26 @@ RequestBatcher.prototype.clear = function() { }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -2525,16 +2416,16 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2635,16 +2526,12 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -2686,7 +2573,8 @@ RequestBatcher.prototype.flush = function(options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2698,12 +2586,12 @@ RequestBatcher.prototype.flush = function(options) { */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4799,22 +4687,69 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -4905,10 +4840,7 @@ MixpanelLib.prototype.init_batchers = function() { return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/examples/es2015-babelify/bundle.js b/examples/es2015-babelify/bundle.js index 5a5e2aa3..eaa0a212 100644 --- a/examples/es2015-babelify/bundle.js +++ b/examples/es2015-babelify/bundle.js @@ -162,13 +162,8 @@ Object.defineProperty(exports, '__esModule', { value: true }); var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; exports['default'] = Config; @@ -1310,22 +1305,66 @@ MixpanelLib.prototype._send_request = function (url, data, options, callback) { } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - (0, _utils.make_xhr_request)({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _utils._.each(headers, function (headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { + // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _utils._.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if (req.timeout && !req.status && new Date().getTime() - start_time >= req.timeout) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({ status: 0, error: error, xhr_req: req }); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -1417,10 +1456,7 @@ MixpanelLib.prototype.init_batchers = function () { if (!this.are_batchers_initialized()) { var batcher_for = _utils._.bind(function (attrs) { return new _requestBatcher.RequestBatcher(attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _utils._.bind(function (data, options, cb) { this._send_request(this.get_config('api_host') + attrs.endpoint, this._encode_data_for_request(data), options, this._prepare_callback(cb, data)); }, this), @@ -4026,23 +4062,22 @@ var logger = (0, _utils.console_with_prefix)('batch'); * @constructor */ var RequestBatcher = function RequestBatcher(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new _requestQueue.RequestQueue(storageKey, { errorReporter: _utils._.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -4053,7 +4088,7 @@ var RequestBatcher = function RequestBatcher(storageKey, options) { * Add one item to queue. */ RequestBatcher.prototype.enqueue = function (item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -4085,27 +4120,27 @@ RequestBatcher.prototype.clear = function () { }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function () { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function () { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function (flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_utils._.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_utils._.bind(this.flush, this), this.flushInterval); } }; @@ -4128,16 +4163,16 @@ RequestBatcher.prototype.flush = function (options) { } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _utils._.each(batch, function (item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -4226,16 +4261,12 @@ RequestBatcher.prototype.flush = function (options) { }), _utils._.bind(function (succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -4275,7 +4306,7 @@ RequestBatcher.prototype.flush = function (options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); } catch (err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -4287,12 +4318,12 @@ RequestBatcher.prototype.flush = function (options) { */ RequestBatcher.prototype.reportError = function (msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch (err) { logger.error(err); } @@ -4339,7 +4370,6 @@ var RequestQueue = function RequestQueue(storageKey, options) { this.reportError = options.errorReporter || _utils._.bind(logger.error, logger); this.lock = new _sharedLock.SharedLock(storageKey, { storage: this.storage }); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -4364,14 +4394,6 @@ RequestQueue.prototype.enqueue = function (item, flushInterval, cb) { 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_utils._.bind(function lockAcquired() { var succeeded; try { @@ -4405,7 +4427,7 @@ RequestQueue.prototype.enqueue = function (item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function (batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -4458,12 +4480,6 @@ RequestQueue.prototype.removeItemsByID = function (ids, cb) { }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } var removeFromStorage = _utils._.bind(function () { var succeeded; @@ -4546,13 +4562,6 @@ var updatePayloads = function updatePayloads(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function (itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_utils._.bind(function lockAcquired() { var succeeded; try { @@ -4614,10 +4623,7 @@ RequestQueue.prototype.saveToStorage = function (queue) { */ RequestQueue.prototype.clear = function () { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; exports.RequestQueue = RequestQueue; @@ -6446,79 +6452,6 @@ var cheap_guid = function cheap_guid(maxlen) { return maxlen ? guid.substring(0, maxlen) : guid; }; -/** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ -var make_xhr_request = function make_xhr_request(options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function (headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { - // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if (req.timeout && !req.status && new Date().getTime() - start_time >= req.timeout) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({ status: 0, error: error, xhr_req: req }); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); -}; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -6583,6 +6516,5 @@ exports.localStorageSupported = localStorageSupported; exports.JSONStringify = JSONStringify; exports.JSONParse = JSONParse; exports.slice = slice; -exports.make_xhr_request = make_xhr_request; },{"./config":3}]},{},[1]); diff --git a/examples/umd-webpack/bundle.js b/examples/umd-webpack/bundle.js index 702bc626..942e026a 100644 --- a/examples/umd-webpack/bundle.js +++ b/examples/umd-webpack/bundle.js @@ -68,13 +68,8 @@ })(this, (function () { 'use strict'; var Config = { -<<<<<<< HEAD DEBUG: false, LIB_VERSION: '2.53.0' -======= - DEBUG: true, - LIB_VERSION: '2.50.0' ->>>>>>> 571d8b5 (cleanup, draft) }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -1752,83 +1747,6 @@ return maxlen ? guid.substring(0, maxlen) : guid; }; - - /** - * Makes an XMLHttpRequest with the given options. - * - * @param {Object} options - Configuration options for the request. - * @param {string} options.method - The HTTP method to use for the request (e.g., 'GET', 'POST'). - * @param {string} options.url - The URL to which the request is sent. - * @param {Object} [options.headers] - Additional headers to include in the request. - * @param {number} [options.timeout_ms] - The timeout for the request in milliseconds. - * @param {boolean} [options.verbose] - Whether to operate in verbose mode, which provides detailed response data. - * @param {boolean} [options.ignore_json_errors] - Whether to ignore JSON parsing errors and return raw response text. - * @param {Function} [options.callback] - The callback function to execute when the request completes. - * @param {Function} [options.report_error] - The function to execute when an error occurs. - * @param {string|Object} [options.body_data] - The data to send with the request, if any. - */ - var make_xhr_request = function (options) { - var req = new XMLHttpRequest(); - req.open(options.method, options.url, true); - - _.each(options.headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (options.callback) { - if (options.verbose) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - options.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - options.callback(response); - } else { - options.callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - options.report_error(error); - if (options.callback) { - if (options.verbose) { - options.callback({status: 0, error: error, xhr_req: req}); - } else { - options.callback(0); - } - } - } - } - }; - req.send(options.body_data); - }; - // naive way to extract domain name (example.com) from full hostname (my.sub.example.com) var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk @@ -2200,7 +2118,6 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); - this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2225,14 +2142,6 @@ 'payload': item }; - if (!this.usePersistence) { - this.memQueue.push(queueEntry); - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2258,7 +2167,6 @@ }, this), this.pid); }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -2267,7 +2175,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (this.usePersistence && batch.length < batchSize) { + if (batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2316,13 +2224,7 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - + var removeFromStorage = _.bind(function() { var succeeded; try { @@ -2404,13 +2306,6 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - if (!this.usePersistence) { - if (cb) { - cb(true); - } - return; - } - this.lock.withLock(_.bind(function lockAcquired() { var succeeded; try { @@ -2472,10 +2367,7 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - - if (this.usePersistence) { - this.storage.removeItem(this.storageKey); - } + this.storage.removeItem(this.storageKey); }; // maximum interval between request retries after exponential backoff @@ -2490,23 +2382,22 @@ * @constructor */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage, - usePersistence: options.usePersistence + storage: options.storage }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = this.options.batchSize; - this.currentFlushInterval = this.options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !this.options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe @@ -2517,7 +2408,7 @@ * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -2549,26 +2440,26 @@ }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -2591,16 +2482,16 @@ } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); - var currentBatchSize = this.currentBatchSize; + var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -2701,16 +2592,12 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { - this.resetFlush(); // schedule next batch with a delay - } else { - this.flush(); // handle next batch if the queue isn't empty - } + this.flush(); // handle next batch if the queue isn't empty } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } @@ -2752,7 +2639,8 @@ requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2764,12 +2652,12 @@ */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } @@ -4865,22 +4753,69 @@ } } else if (USE_XHR) { try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + var headers = this.get_config('xhr_headers'); if (use_post) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; } - - make_xhr_request({ - method: options.method, - url: url, - headers: headers, - timeout_ms: options.timeout_ms, - verbose_mode: verbose_mode, - ignore_json_errors: options.ignore_json_errors, - callback: callback, - report_error: lib.report_error, - body_data: body_data + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); } catch (e) { lib.report_error(e); succeeded = false; @@ -4971,10 +4906,7 @@ return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 5451385c..3475f329 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -796,7 +796,7 @@ MixpanelLib.prototype.init_batchers = function() { }, this), errorReporter: this.get_config('error_reporter'), stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), - usePersistence: true, + usePersistence: true } ); }, this); From 309f10221a22429f65fb7b61a2c1f81c618a224c Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 25 Jun 2024 15:44:09 +0000 Subject: [PATCH 13/48] rebase + simple feedback --- src/recorder/index.js | 79 ++++---------- src/request-queue.js | 190 +++++++++++++++++----------------- src/utils.js | 2 +- tests/unit/request-batcher.js | 8 +- 4 files changed, 120 insertions(+), 159 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index cdb1fdfb..37ea47a3 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -39,9 +39,7 @@ MixpanelRecorder.prototype._initBatcher = function () { flushIntervalMs: BATCH_FLUSH_INTERVAL_MS, requestTimeoutMs: BATCH_REQUEST_TIMEOUT_MS, autoStart: true, - sendRequestFunc: _.bind(function(data, options, callback) { - this.sendRequestWithOptOut(data, options, callback); - }, this), + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), forceDelayFlush: true, }); }; @@ -122,8 +120,8 @@ MixpanelRecorder.prototype.stopRecording = function () { * Flushes the current batch of events to the server, but passes an opt-out callback to make sure * we stop recording and dump any queued events if the user has opted out. */ -MixpanelRecorder.prototype.sendRequestWithOptOut = function (data, options, cb) { - this._sendRequest(data, options, cb, _.bind(this._onOptOut, this)); +MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); }; MixpanelRecorder.prototype._onOptOut = function (code) { @@ -134,33 +132,38 @@ MixpanelRecorder.prototype._onOptOut = function (code) { } }; -MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) { +MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { 'method': 'POST', 'headers': { 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), 'Content-Type': 'application/octet-stream' }, - 'body': reqBody + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + callback({status: response.status, responseBody: responseBody, retryAfter: response.headers.get('Retry-After')}); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); }; -/** - * @api private - * Private method, flushes the current batch of events to the server. - */ -MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() { - var numEvents = this.recEvents.length; +MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + if (numEvents > 0) { var reqParams = { 'distinct_id': String(this._mixpanel.get_distinct_id()), 'seq': this.seqNo++, - 'batch_start_time': this.batchStartTime / 1000, + 'batch_start_time': this.batchStartTime / 1000, // TODO: fix batch start time 'replay_id': this.replayId, 'replay_length_ms': this.replayLengthMs, 'replay_start_time': this.replayStartTime / 1000 }; - var eventsJson = _.JSONEncode(this.recEvents); + var eventsJson = _.JSONEncode(data); // send ID management props if they exist var deviceId = this._mixpanel.get_property('$device_id'); @@ -172,63 +175,23 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() { reqParams['$user_id'] = userId; } - this.recEvents = this.recEvents.slice(numEvents); - this.batchStartTime = new Date().getTime(); if (CompressionStream) { + reqParams['format'] = 'gzip'; var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); new Response(gzipStream) .blob() .then(_.bind(function(compressedBlob) { reqParams['format'] = 'gzip'; - this._sendRequest(reqParams, compressedBlob); + this._sendRequest(reqParams, compressedBlob, callback); }, this)); } else { reqParams['format'] = 'body'; - this._sendRequest(reqParams, eventsJson); + this._sendRequest(reqParams, eventsJson, callback); } } }); -MixpanelRecorder.prototype._sendRequest = addOptOutCheckMixpanelLib(function (data, options, callback) { - var reqBody = { - 'distinct_id': String(this._mixpanel.get_distinct_id()), - 'events': data, - 'seq': this.seqNo++, - 'batch_start_time': this.batchStartTime / 1000, - 'replay_id': this.replayId, - 'replay_length_ms': this.replayLengthMs, - 'replay_start_time': this.replayStartTime / 1000 - }; - - // send ID management props if they exist - var deviceId = this._mixpanel.get_property('$device_id'); - if (deviceId) { - reqBody['$device_id'] = deviceId; - } - var userId = this._mixpanel.get_property('$user_id'); - if (userId) { - reqBody['$user_id'] = userId; - } - - window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'], { - 'method': 'POST', - 'headers': { - 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), - 'Content-Type': 'application/json' - }, - 'body': _.JSONEncode(reqBody) - }).then(function (response) { - response.json().then(function (responseBody) { - callback({status: response.status, responseBody: responseBody, retryAfter: response.headers.get('Retry-After')}); - }).catch(function (error) { - callback({error: error}); - }); - }).catch(function (error) { - callback({error: error}); - }); -}); - MixpanelRecorder.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); diff --git a/src/request-queue.js b/src/request-queue.js index afad17f8..bdb870a6 100644 --- a/src/request-queue.js +++ b/src/request-queue.js @@ -56,35 +56,33 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { if (cb) { cb(true); } - return; - } - - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; - /** * Read out the given number of queue entries. If this.memQueue * has fewer than batchSize items, then look for "orphaned" items @@ -146,63 +144,63 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { if (cb) { cb(true); } - return; - } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -234,28 +232,28 @@ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { if (cb) { cb(true); } - return; + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); } - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); }; /** diff --git a/src/utils.js b/src/utils.js index 877e29d0..af6a76db 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1737,5 +1737,5 @@ export { localStorageSupported, JSONStringify, JSONParse, - slice, + slice }; diff --git a/tests/unit/request-batcher.js b/tests/unit/request-batcher.js index 8c9dcf8a..0b7006ed 100644 --- a/tests/unit/request-batcher.js +++ b/tests/unit/request-batcher.js @@ -5,8 +5,8 @@ import sinonChai from 'sinon-chai'; chai.use(sinonChai); -import { RequestBatcher } from '../../src/request-batcher'; -import {assign, mapValues} from 'lodash'; +import {RequestBatcher} from '../../src/request-batcher'; +import {mapValues} from 'lodash'; const LOCALSTORAGE_KEY = `fake-rb-key`; const START_TIME = 100000; @@ -49,7 +49,7 @@ describe(`RequestBatcher`, function() { requestTimeoutMs: REQUEST_TIMEOUT_MS, }; - const options = assign({}, defaultOptions, optionOverrides); + const options = Object.assign({}, defaultOptions, optionOverrides); batcher = new RequestBatcher(LOCALSTORAGE_KEY, options); } @@ -90,7 +90,7 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}, function(succeeded) { expect(succeeded).to.be.ok; expect(batcher.queue.memQueue).to.have.lengthOf(1); - expect(getLocalStorageItems()).to.not.be.ok; + expect(getLocalStorageItems()).to.be.null; const queuedEntry = batcher.queue.memQueue[0]; expect(queuedEntry.flushAfter).to.be.greaterThan(START_TIME + 5000); expect(queuedEntry.flushAfter).to.be.lessThan(START_TIME + 15000); From 43f85da63c48fa2aa437ac8f08d407fec1bdeaa9 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 25 Jun 2024 22:18:05 +0000 Subject: [PATCH 14/48] bring back libConfig because of the reference stuff --- src/mixpanel-core.js | 7 ++-- src/recorder/index.js | 21 +++++++---- src/request-batcher.js | 82 +++++++++++++++++------------------------- 3 files changed, 49 insertions(+), 61 deletions(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 3475f329..2e2fbfdb 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -779,10 +779,8 @@ MixpanelLib.prototype.init_batchers = function() { return new RequestBatcher( attrs.queue_key, { - batchSize: this.get_config('batch_size'), - flushIntervalMs: this.get_config('batch_flush_interval_ms'), - requestTimeoutMs: this.get_config('batch_request_timeout_ms'), - autoStart: this.get_config('batch_autostart'), + libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -794,7 +792,6 @@ MixpanelLib.prototype.init_batchers = function() { beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), usePersistence: true } diff --git a/src/recorder/index.js b/src/recorder/index.js index 37ea47a3..14689675 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -21,7 +21,6 @@ var MixpanelRecorder = function(mixpanelInstance) { this.seqNo = 0; this.replayId = null; this.replayStartTime = null; - this.batchStartTime = null; this.replayLengthMs = 0; this.sendBatchId = null; @@ -34,13 +33,19 @@ var MixpanelRecorder = function(mixpanelInstance) { MixpanelRecorder.prototype._initBatcher = function () { + var libConfig = { + 'batch_size': BATCH_SIZE, + 'batch_flush_interval_ms': BATCH_FLUSH_INTERVAL_MS, + 'batch_request_timeout_ms': BATCH_REQUEST_TIMEOUT_MS, + 'batch_autostart': true, + }; + this.batcher = new RequestBatcher('__mprec', { - batchSize: BATCH_SIZE, - flushIntervalMs: BATCH_FLUSH_INTERVAL_MS, - requestTimeoutMs: BATCH_REQUEST_TIMEOUT_MS, - autoStart: true, + libConfig: libConfig, sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), forceDelayFlush: true, + usePersistence: false, }); }; @@ -65,7 +70,6 @@ MixpanelRecorder.prototype.startRecording = function () { this.seqNo = 0; this.startDate = new Date(); this.replayStartTime = this.startDate.getTime(); - this.batchStartTime = this.replayStartTime; this.replayId = _.UUID(); this.replayLengthMs = 0; @@ -155,10 +159,13 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da const numEvents = data.length; if (numEvents > 0) { + // TODO(jakub): make time props based on rrweb events for max accuracy + var batchStartTime = data[0].timestamp; + var reqParams = { 'distinct_id': String(this._mixpanel.get_distinct_id()), 'seq': this.seqNo++, - 'batch_start_time': this.batchStartTime / 1000, // TODO: fix batch start time + 'batch_start_time': batchStartTime / 1000, 'replay_id': this.replayId, 'replay_length_ms': this.replayLengthMs, 'replay_start_time': this.replayStartTime / 1000 diff --git a/src/request-batcher.js b/src/request-batcher.js index 80f0dc4c..f4d672a0 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -12,56 +12,40 @@ var logger = console_with_prefix('batch'); * type (events, people, groups). * Uses RequestQueue to manage the backing store. * @constructor - * @param {string} storageKey - Key to access the storage for request queue. - * @param {Object} options - Configuration options for the RequestBatcher. - * @param {number} options.batchSize - The size of the batch to be sent in each flush. - * @param {number} options.flushIntervalMs - Interval in milliseconds between each flush attempt. - * @param {boolean} options.usePersistence - Whether to use persistent storage. - * @param {boolean} options.forceDelayFlush - Force flush at the interval specified by flushIntervalMs. - * @param {boolean} options.autoStart - Automatically start the batcher upon initialization. - * @param {Object} options.storage - Storage implementation to use. - * @param {function} options.errorReporter - Function to report errors. - * @param {function} options.sendRequestFunc - Function to send the request. Takes three arguments: - * - data: Array of payload objects to be sent. - * - requestOptions: Object containing request options (method, timeout, transport type, etc.). - * - callback: Function to be called with the response. Should be called with an object containing optional fields: - * - {number} [status] - HTTP status code of the response. - * - {string} [error] - Error message if the request failed. - * - {string} [retryAfter] - Value of the 'Retry-After' header - * - {Object|string} [responseBody] - Body of the response. - * @param {function} options.stopAllBatchingFunc - Function to stop all batching operations. - * @param {function} [options.beforeSendHook] - Hook to modify payload before sending. - * @param {number} [options.requestTimeoutMs] - Timeout for each request. */ var RequestBatcher = function(storageKey, options) { - this.options = options; - + this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), storage: options.storage, - usePersistence: options.usePersistence + usePersistence: options.usePersistence, }); - // seed variable batch size + flush interval with configured values - this.currentBatchSize = options.batchSize; - this.currentFlushInterval = options.flushIntervalMs; + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; - this.stopped = !options.autoStart; + this.stopped = !this.libConfig['batch_autostart']; this.consecutiveRemovalFailures = 0; // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-volume events like Session Replay. + this.forceDelayFlush = options.forceDelayFlush || false; }; /** * Add one item to queue. */ RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.currentFlushInterval, cb); + this.queue.enqueue(item, this.flushInterval, cb); }; /** @@ -93,26 +77,26 @@ RequestBatcher.prototype.clear = function() { }; /** - * Restore batch size configuration to the originally initialized value + * Restore batch size configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetBatchSize = function() { - this.currentBatchSize = this.options.batchSize; + this.batchSize = this.libConfig['batch_size']; }; /** - * Restore flush interval time configuration to the originally initialized value + * Restore flush interval time configuration to whatever is set in the main SDK. */ RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.options.flushIntervalMs); + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); }; /** * Schedule the next flush in the given number of milliseconds. */ RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.currentFlushInterval = flushMS; + this.flushInterval = flushMS; if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.currentFlushInterval); + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); } }; @@ -135,16 +119,16 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.options.requestTimeoutMs; + var timeoutMS = this.requestTimeoutMs; var startTime = new Date().getTime(); - var flushBatchSize = this.currentBatchSize; - var batch = this.queue.fillBatch(flushBatchSize); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { var payload = item['payload']; - if (this.options.beforeSendHook && !item.orphaned) { - payload = this.options.beforeSendHook(payload); + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); } if (payload) { // mp_sent_by_lib_version prop captures which lib version actually @@ -209,7 +193,7 @@ RequestBatcher.prototype.flush = function(options) { (res.status >= 500 || res.status === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry - var retryMS = this.currentFlushInterval * 2; + var retryMS = this.flushInterval * 2; if (res.retryAfter) { retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } @@ -219,8 +203,8 @@ RequestBatcher.prototype.flush = function(options) { } else if (_.isObject(res) && res.status === 413) { // 413 Payload Too Large if (batch.length > 1) { - var halvedBatchSize = Math.max(1, Math.floor(flushBatchSize / 2)); - this.currentBatchSize = Math.min(this.currentBatchSize, halvedBatchSize, batch.length - 1); + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); this.reportError('413 response; reducing batch size to ' + this.batchSize); this.resetFlush(); } else { @@ -249,7 +233,7 @@ RequestBatcher.prototype.flush = function(options) { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.options.stopAllBatchingFunc(); + this.stopAllBatchingFunc(); } else { this.resetFlush(); } @@ -291,7 +275,7 @@ RequestBatcher.prototype.flush = function(options) { requestOptions.transport = 'sendBeacon'; } logger.log('MIXPANEL REQUEST:', dataForRequest); - this.options.sendRequestFunc(dataForRequest, requestOptions, batchSendCallback); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -303,12 +287,12 @@ RequestBatcher.prototype.flush = function(options) { */ RequestBatcher.prototype.reportError = function(msg, err) { logger.error.apply(logger.error, arguments); - if (this.options.errorReporter) { + if (this.errorReporter) { try { if (!(err instanceof Error)) { err = new Error(msg); } - this.options.errorReporter(msg, err); + this.errorReporter(msg, err); } catch(err) { logger.error(err); } From 7a06a22fdb426c554ce502bfea25be4a85c36d77 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 25 Jun 2024 22:37:48 +0000 Subject: [PATCH 15/48] leverage the rrweb timestamp property --- src/recorder/index.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 14689675..213f1f80 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -7,9 +7,12 @@ import { RequestBatcher } from '../request-batcher'; var logger = console_with_prefix('recorder'); var CompressionStream = window['CompressionStream']; -var BATCH_SIZE = 1000; -var BATCH_FLUSH_INTERVAL_MS = 10 * 1000; -var BATCH_REQUEST_TIMEOUT_MS = 90 * 1000; +var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true, +}; var MixpanelRecorder = function(mixpanelInstance) { this._mixpanel = mixpanelInstance; @@ -21,7 +24,6 @@ var MixpanelRecorder = function(mixpanelInstance) { this.seqNo = 0; this.replayId = null; this.replayStartTime = null; - this.replayLengthMs = 0; this.sendBatchId = null; this.idleTimeoutId = null; @@ -33,15 +35,8 @@ var MixpanelRecorder = function(mixpanelInstance) { MixpanelRecorder.prototype._initBatcher = function () { - var libConfig = { - 'batch_size': BATCH_SIZE, - 'batch_flush_interval_ms': BATCH_FLUSH_INTERVAL_MS, - 'batch_request_timeout_ms': BATCH_REQUEST_TIMEOUT_MS, - 'batch_autostart': true, - }; - this.batcher = new RequestBatcher('__mprec', { - libConfig: libConfig, + libConfig: RECORDER_BATCHER_LIB_CONFIG, sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), errorReporter: _.bind(this.reportError, this), forceDelayFlush: true, @@ -68,11 +63,9 @@ MixpanelRecorder.prototype.startRecording = function () { this.recEvents = []; this.seqNo = 0; - this.startDate = new Date(); - this.replayStartTime = this.startDate.getTime(); + this.replayStartTime = null; this.replayId = _.UUID(); - this.replayLengthMs = 0; this.batcher.start(); @@ -87,7 +80,6 @@ MixpanelRecorder.prototype.startRecording = function () { this._stopRecording = record({ 'emit': _.bind(function (ev) { this.batcher.enqueue(ev); - this.replayLengthMs = new Date().getTime() - this.replayStartTime; resetIdleTimeout(); }, this), 'maskAllInputs': true, @@ -159,15 +151,19 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da const numEvents = data.length; if (numEvents > 0) { - // TODO(jakub): make time props based on rrweb events for max accuracy + // each rrweb event has a timestamp - leverage those to get time properties var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; var reqParams = { 'distinct_id': String(this._mixpanel.get_distinct_id()), 'seq': this.seqNo++, 'batch_start_time': batchStartTime / 1000, 'replay_id': this.replayId, - 'replay_length_ms': this.replayLengthMs, + 'replay_length_ms': replayLengthMs, 'replay_start_time': this.replayStartTime / 1000 }; var eventsJson = _.JSONEncode(data); From 4b909c6b6ab44b1cfe2f2e220e3214950b215ef7 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 25 Jun 2024 23:00:38 +0000 Subject: [PATCH 16/48] attempt secondary flush immediately in some cases --- src/recorder/index.js | 2 +- src/request-batcher.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 213f1f80..9d7f785c 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -39,7 +39,7 @@ MixpanelRecorder.prototype._initBatcher = function () { libConfig: RECORDER_BATCHER_LIB_CONFIG, sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), errorReporter: _.bind(this.reportError, this), - forceDelayFlush: true, + flushOnlyOnInterval: true, usePersistence: false, }); }; diff --git a/src/request-batcher.js b/src/request-batcher.js index f4d672a0..dcc2b203 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -36,9 +36,10 @@ var RequestBatcher = function(storageKey, options) { // extra client-side dedupe this.itemIdsSentSuccessfully = {}; - // Forces flush to occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes - // as long as the queue is not empty. This is useful for high-volume events like Session Replay. - this.forceDelayFlush = options.forceDelayFlush || false; + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -123,6 +124,9 @@ RequestBatcher.prototype.flush = function(options) { var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -224,7 +228,7 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - if (this.forceDelayFlush) { + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { this.resetFlush(); // schedule next batch with a delay } else { this.flush(); // handle next batch if the queue isn't empty From dfe8dc598554dd285b07d59bdc72595e2edad7ea Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 26 Jun 2024 14:43:28 +0000 Subject: [PATCH 17/48] fix unit tests --- src/request-batcher.js | 4 +- tests/unit/request-batcher.js | 251 +++++++++++++++++----------------- 2 files changed, 127 insertions(+), 128 deletions(-) diff --git a/src/request-batcher.js b/src/request-batcher.js index dcc2b203..338454a4 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -120,7 +120,7 @@ RequestBatcher.prototype.flush = function(options) { } options = options || {}; - var timeoutMS = this.requestTimeoutMs; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); @@ -237,7 +237,7 @@ RequestBatcher.prototype.flush = function(options) { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatchingFunc(); + this.stopAllBatching(); } else { this.resetFlush(); } diff --git a/tests/unit/request-batcher.js b/tests/unit/request-batcher.js index 0b7006ed..83d5bf4d 100644 --- a/tests/unit/request-batcher.js +++ b/tests/unit/request-batcher.js @@ -5,7 +5,7 @@ import sinonChai from 'sinon-chai'; chai.use(sinonChai); -import {RequestBatcher} from '../../src/request-batcher'; +import { RequestBatcher } from '../../src/request-batcher'; import {mapValues} from 'lodash'; const LOCALSTORAGE_KEY = `fake-rb-key`; @@ -19,7 +19,7 @@ describe(`RequestBatcher`, function() { let clock = null; function configureBatchSize(batchSize) { - batcher.options.batchSize = batchSize; + libConfig.batch_size = batchSize; batcher.resetBatchSize(); } @@ -29,8 +29,8 @@ describe(`RequestBatcher`, function() { function sendResponse(status, {error, retryAfter} = {}) { // respond to last request sent - const requestIndex = batcher.options.sendRequestFunc.args.length - 1; - batcher.options.sendRequestFunc.args[requestIndex][2]({ + const requestIndex = batcher.sendRequest.args.length - 1; + batcher.sendRequest.args[requestIndex][2]({ status, retryAfter, error, @@ -39,18 +39,19 @@ describe(`RequestBatcher`, function() { function initBatcher(optionOverrides) { optionOverrides = optionOverrides || {}; - const defaultOptions = { + libConfig = { + batch_flush_interval_ms: DEFAULT_FLUSH_INTERVAL, + batch_request_timeout_ms: REQUEST_TIMEOUT_MS, + batch_size: 50, + batch_autostart: true, + }; + + batcher = new RequestBatcher(LOCALSTORAGE_KEY, Object.assign({ + libConfig, sendRequestFunc: sinon.spy(), storage: localStorage, usePersistence: true, - flushIntervalMs: DEFAULT_FLUSH_INTERVAL, - batchSize: 50, - autoStart: true, - requestTimeoutMs: REQUEST_TIMEOUT_MS, - }; - - const options = Object.assign({}, defaultOptions, optionOverrides); - batcher = new RequestBatcher(LOCALSTORAGE_KEY, options); + }, optionOverrides)); } beforeEach(function() { @@ -75,9 +76,7 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}, function(succeeded) { expect(succeeded).to.be.ok; expect(batcher.queue.memQueue).to.have.lengthOf(1); - expect(getLocalStorageItems()).to.have.lengthOf(1); const queuedEntry = batcher.queue.memQueue[0]; - expect(queuedEntry).to.eql(getLocalStorageItems()[0]); expect(queuedEntry.flushAfter).to.be.greaterThan(START_TIME + 5000); expect(queuedEntry.flushAfter).to.be.lessThan(START_TIME + 15000); expect(queuedEntry.payload).to.deep.equal({foo: `bar`}); @@ -103,14 +102,14 @@ describe(`RequestBatcher`, function() { describe(`flush`, function() { it(`does not call sendRequest when queue is empty`, function() { batcher.flush(); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; }); it(`calls sendRequest with items to flush`, function() { batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([{foo: `bar`}]); }); it(`removes items from queue on successful response`, function() { @@ -125,12 +124,12 @@ describe(`RequestBatcher`, function() { }); it(`transforms items before sending if a hook function has been provided`, function() { - batcher.options.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); + batcher.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); batcher.enqueue({Hello: `World`}); batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([ {Hello: `WORLD`}, {foo: `BAR`}, ]); @@ -144,48 +143,48 @@ describe(`RequestBatcher`, function() { batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; sendResponse(200); // second request should follow immediately - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.sendRequest).to.have.been.calledTwice; sendResponse(200); - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; // forsooth + expect(batcher.sendRequest).to.have.been.calledThrice; // forsooth // check what was sent in those requests - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ + expect(batcher.sendRequest.args[0][0]).to.deep.equal([ {ev: `queued event 1`}, {ev: `queued event 2`}, {ev: `queued event 3`}, ]); - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ + expect(batcher.sendRequest.args[1][0]).to.deep.equal([ {ev: `queued event 4`}, {ev: `queued event 5`}, {ev: `queued event 6`}, ]); - expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ + expect(batcher.sendRequest.args[2][0]).to.deep.equal([ {ev: `queued event 7`}, {ev: `queued event 8`}, ]); // no new requests after that clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; + expect(batcher.sendRequest).to.have.been.calledThrice; }); it(`prevents reentrant flushes`, function() { batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; batcher.enqueue({foo2: `bar2`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request + expect(batcher.sendRequest).to.have.been.calledOnce; // no new request sendResponse(200); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.sendRequest).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([{foo: `bar`}]); - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([{foo2: `bar2`}]); + expect(batcher.sendRequest.args[0][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.sendRequest.args[1][0]).to.deep.equal([{foo2: `bar2`}]); }); describe(`error handling`, function() { @@ -193,50 +192,50 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.enqueue({foo2: `bar2`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; sendResponse(500); clock.tick(DEFAULT_FLUSH_INTERVAL); // no new requests, items are still in queue - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL); // retry with same data - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); + expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.sendRequest.args[1][0]).to.deep.equal(batcher.sendRequest.args[0][0]); // oh no, another explosion! sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); // no new requests, items are still in queue - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.sendRequest).to.have.been.calledTwice; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); // retry with same data - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; - expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); + expect(batcher.sendRequest).to.have.been.calledThrice; + expect(batcher.sendRequest.args[2][0]).to.deep.equal(batcher.sendRequest.args[0][0]); // do it again, oh the humanity sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 4); // no new requests, items are still in queue - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; + expect(batcher.sendRequest).to.have.been.calledThrice; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL * 4); // retry with same data - expect(batcher.options.sendRequestFunc).to.have.callCount(4); - expect(batcher.options.sendRequestFunc.args[3][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); + expect(batcher.sendRequest).to.have.callCount(4); + expect(batcher.sendRequest.args[3][0]).to.deep.equal(batcher.sendRequest.args[0][0]); // will the madness ever end? finally the API call succeeds sendResponse(200); clock.tick(DEFAULT_FLUSH_INTERVAL * 100); // a long time - expect(batcher.options.sendRequestFunc).to.have.callCount(4); // no new requests + expect(batcher.sendRequest).to.have.callCount(4); // no new requests expect(batcher.queue.memQueue).to.be.empty; expect(getLocalStorageItems()).to.be.empty; }); @@ -249,7 +248,7 @@ describe(`RequestBatcher`, function() { let tryAfter = DEFAULT_FLUSH_INTERVAL * 2; const TEN_MINUTES = 10 * 60 * 1000; while (tryAfter <= TEN_MINUTES) { - expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); + expect(batcher.sendRequest).to.have.callCount(expectedRequests); sendResponse(503); clock.tick(tryAfter); @@ -257,20 +256,20 @@ describe(`RequestBatcher`, function() { expectedRequests++; } - expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); + expect(batcher.sendRequest).to.have.callCount(expectedRequests); sendResponse(503); clock.tick(TEN_MINUTES - 1); - expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); // no new request + expect(batcher.sendRequest).to.have.callCount(expectedRequests); // no new request clock.tick(1); - expect(batcher.options.sendRequestFunc).to.have.callCount(++expectedRequests); + expect(batcher.sendRequest).to.have.callCount(++expectedRequests); // do it again, exactly 10 minutes til next request - expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); + expect(batcher.sendRequest).to.have.callCount(expectedRequests); sendResponse(503); clock.tick(TEN_MINUTES - 1); - expect(batcher.options.sendRequestFunc).to.have.callCount(expectedRequests); // no new request + expect(batcher.sendRequest).to.have.callCount(expectedRequests); // no new request clock.tick(1); - expect(batcher.options.sendRequestFunc).to.have.callCount(++expectedRequests); + expect(batcher.sendRequest).to.have.callCount(++expectedRequests); }); it(`resets flush interval when request succeeds after backoff`, function() { @@ -280,31 +279,31 @@ describe(`RequestBatcher`, function() { // fail a couple times clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.sendRequest).to.have.been.calledTwice; sendResponse(503); // configuring default flush interval shouldn't affect anything during failure backoff - batcher.options.flushIntervalMs = 8000; + libConfig.batch_flush_interval_ms = 8000; clock.tick(DEFAULT_FLUSH_INTERVAL * 4); - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; + expect(batcher.sendRequest).to.have.been.calledThrice; sendResponse(503); // succeed! clock.tick(DEFAULT_FLUSH_INTERVAL * 8); - expect(batcher.options.sendRequestFunc).to.have.callCount(4); + expect(batcher.sendRequest).to.have.callCount(4); sendResponse(200); // at this point the success response should have reset the interval to the 8000 // configured above batcher.enqueue({ev: `queued event 3`}); clock.tick(7000); - expect(batcher.options.sendRequestFunc).to.have.callCount(4); // no new request yet + expect(batcher.sendRequest).to.have.callCount(4); // no new request yet clock.tick(1000); - expect(batcher.options.sendRequestFunc).to.have.callCount(5); + expect(batcher.sendRequest).to.have.callCount(5); }); it(`can queue up new events while failing requests are retrying`, function() { @@ -314,19 +313,19 @@ describe(`RequestBatcher`, function() { // fail a couple times clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; sendResponse(503); clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.sendRequest).to.have.been.calledTwice; sendResponse(503); batcher.enqueue({ev: `queued event 3`}); clock.tick(DEFAULT_FLUSH_INTERVAL * 4); - expect(batcher.options.sendRequestFunc).to.have.callCount(3); + expect(batcher.sendRequest).to.have.callCount(3); // should include all events in current retry - expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ + expect(batcher.sendRequest.args[2][0]).to.deep.equal([ {ev: `queued event 1`}, {ev: `queued event 2`}, {ev: `queued event 3`}, @@ -343,7 +342,7 @@ describe(`RequestBatcher`, function() { sendResponse(400); clock.tick(100000); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request + expect(batcher.sendRequest).to.have.been.calledOnce; // no new request expect(batcher.queue.memQueue).to.be.empty; expect(getLocalStorageItems()).to.be.empty; }); @@ -358,7 +357,7 @@ describe(`RequestBatcher`, function() { sendResponse(0); clock.tick(100000); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request + expect(batcher.sendRequest).to.have.been.calledOnce; // no new request expect(batcher.queue.memQueue).to.be.empty; expect(getLocalStorageItems()).to.be.empty; }); @@ -367,19 +366,19 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.enqueue({foo2: `bar2`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; sendResponse(429); clock.tick(DEFAULT_FLUSH_INTERVAL); // no new requests, items are still in queue - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; expect(batcher.queue.memQueue).to.have.lengthOf(2); expect(getLocalStorageItems()).to.have.lengthOf(2); clock.tick(DEFAULT_FLUSH_INTERVAL); // retry with same data - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal(batcher.options.sendRequestFunc.args[0][0]); + expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.sendRequest.args[1][0]).to.deep.equal(batcher.sendRequest.args[0][0]); }); it(`reduces batch size after 413 Payload Too Large`, function() { @@ -391,13 +390,13 @@ describe(`RequestBatcher`, function() { batcher.flush(); // should have tried to send all 7 items in one go - expect(batcher.options.sendRequestFunc.args[0][0]).to.have.lengthOf(7); + expect(batcher.sendRequest.args[0][0]).to.have.lengthOf(7); sendResponse(413); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; // no backoff + expect(batcher.sendRequest).to.have.been.calledTwice; // no backoff // reduced batch size - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ + expect(batcher.sendRequest.args[1][0]).to.deep.equal([ {ev: `queued event 1`}, {ev: `queued event 2`}, {ev: `queued event 3`}, @@ -406,9 +405,9 @@ describe(`RequestBatcher`, function() { sendResponse(200); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; + expect(batcher.sendRequest).to.have.been.calledThrice; // remaining items from original batch - expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ + expect(batcher.sendRequest.args[2][0]).to.deep.equal([ {ev: `queued event 5`}, {ev: `queued event 6`}, {ev: `queued event 7`}, @@ -418,17 +417,17 @@ describe(`RequestBatcher`, function() { it(`does not retry single item which produces 413 Payload Too Large`, function() { batcher.enqueue({ev: `bloated item`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; sendResponse(413); clock.tick(240000); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request + expect(batcher.sendRequest).to.have.been.calledOnce; // no new request // first item should have been dropped, and we resume normal batching batcher.enqueue({ev: `normal item 1`}); batcher.enqueue({ev: `normal item 2`}); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.sendRequest.args[1][0]).to.deep.equal([ {ev: `normal item 1`}, {ev: `normal item 2`}, ]); @@ -441,22 +440,22 @@ describe(`RequestBatcher`, function() { sendResponse(503, {retryAfter: `20`}); clock.tick(10000); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no retry yet + expect(batcher.sendRequest).to.have.been.calledOnce; // no retry yet clock.tick(10000); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; // 20s have passed + expect(batcher.sendRequest).to.have.been.calledTwice; // 20s have passed // after success, should reset to configured flush interval sendResponse(200); batcher.enqueue({ev: `queued event 3`}); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledThrice; - expect(batcher.options.sendRequestFunc.args[2][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledThrice; + expect(batcher.sendRequest.args[2][0]).to.deep.equal([ {ev: `queued event 3`}, ]); }); it(`handles failures to remove items from queue and eventually stops batchers`, function() { - batcher.options.stopAllBatchingFunc = sinon.spy(); + batcher.stopAllBatching = sinon.spy(); batcher.enqueue({foo: `bar`}); batcher.flush(); @@ -476,36 +475,36 @@ describe(`RequestBatcher`, function() { expect(batcher.consecutiveRemovalFailures).to.equal(1); // no immediate flush - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; // make the event orphaned so we try to send it again clock.tick(DEFAULT_FLUSH_INTERVAL * 3); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; + expect(batcher.sendRequest).to.have.been.calledTwice; sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(2); // now it will try to send on every flush clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.callCount(3); + expect(batcher.sendRequest).to.have.callCount(3); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(3); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.callCount(4); + expect(batcher.sendRequest).to.have.callCount(4); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(4); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.callCount(5); + expect(batcher.sendRequest).to.have.callCount(5); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(5); - expect(batcher.options.stopAllBatchingFunc).not.to.have.been.called; + expect(batcher.stopAllBatching).not.to.have.been.called; clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.callCount(6); + expect(batcher.sendRequest).to.have.callCount(6); sendResponse(200); expect(batcher.consecutiveRemovalFailures).to.equal(6); - expect(batcher.options.stopAllBatchingFunc).to.have.been.calledOnce; + expect(batcher.stopAllBatching).to.have.been.calledOnce; }); context(`when request times out`, function() { @@ -527,8 +526,8 @@ describe(`RequestBatcher`, function() { batcher.flush(); clock.tick(REQUEST_TIMEOUT_MS); timeOutRequest(); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.sendRequest.args[1][0]).to.deep.equal([{foo: `bar`}]); }); it(`checks clock before treating it as a real timeout`, function() { @@ -537,17 +536,17 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.flush(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; timeOutRequest(); // no new request; there was no significant time between sending // the original request and getting the "timeout" response - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; + expect(batcher.sendRequest).to.have.been.calledOnce; // should have been treated like a normal error and backed off clock.tick(DEFAULT_FLUSH_INTERVAL * 2); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.sendRequest.args[1][0]).to.deep.equal([{foo: `bar`}]); }); }); }); @@ -557,15 +556,15 @@ describe(`RequestBatcher`, function() { it(`does not flush`, function() { batcher.enqueue({foo: `bar`}); clock.tick(20000); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; }); it(`flushes immediately on start`, function() { batcher.enqueue({foo: `bar`}); clock.tick(20000); batcher.start(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([{foo: `bar`}]); + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([{foo: `bar`}]); }); }); @@ -576,16 +575,16 @@ describe(`RequestBatcher`, function() { it(`does not send requests until flush interval`, function() { batcher.enqueue({first: `event`}); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; clock.tick(1000); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; batcher.enqueue({second: `event`}); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; clock.tick(1000); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([ {first: `event`}, {second: `event`}, ]); }); @@ -601,10 +600,10 @@ describe(`RequestBatcher`, function() { // kill it localStorage.removeItem(LOCALSTORAGE_KEY); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([ {name: `storagetest 1`}, {name: `storagetest 2`}, ]); @@ -623,8 +622,8 @@ describe(`RequestBatcher`, function() { ])); batcher.start(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - const batchEvents = batcher.options.sendRequestFunc.args[0][0]; + expect(batcher.sendRequest).to.have.been.calledOnce; + const batchEvents = batcher.sendRequest.args[0][0]; expect(batchEvents).to.have.lengthOf(2); expect(batchEvents[0].event).to.equal(`orphaned event 1`); expect(batchEvents[1].event).to.equal(`orphaned event 2`); @@ -645,8 +644,8 @@ describe(`RequestBatcher`, function() { ])); batcher.start(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - const batchEvents = batcher.options.sendRequestFunc.args[0][0]; + expect(batcher.sendRequest).to.have.been.calledOnce; + const batchEvents = batcher.sendRequest.args[0][0]; expect(batchEvents).to.have.lengthOf(1); expect(batchEvents[0].event).to.equal(`orphaned event 1`); @@ -668,13 +667,13 @@ describe(`RequestBatcher`, function() { batcher.start(); clock.tick(20000); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; // first event becomes orphaned clock.tick(80000); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - const payload = batcher.options.sendRequestFunc.args[0][0]; + expect(batcher.sendRequest).to.have.been.calledOnce; + const payload = batcher.sendRequest.args[0][0]; expect(payload).to.have.lengthOf(1); expect(payload[0]).to.have.property(`event`, `orphaned event 1`); expect(payload[0]).to.have.nested.include({'properties.foo': `bar`}); @@ -684,13 +683,13 @@ describe(`RequestBatcher`, function() { expect(getLocalStorageItems()).to.have.lengthOf(1); clock.tick(20000); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; // no new request + expect(batcher.sendRequest).to.have.been.calledOnce; // no new request // second event becomes orphaned clock.tick(200000); - expect(batcher.options.sendRequestFunc).to.have.been.calledTwice; - expect(batcher.options.sendRequestFunc.args[1][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledTwice; + expect(batcher.sendRequest.args[1][0]).to.deep.equal([ {'event': `orphaned event 2`}, ]); @@ -700,7 +699,7 @@ describe(`RequestBatcher`, function() { }); it(`does not apply before-send hooks to orphaned items`, function() { - batcher.options.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); + batcher.beforeSendHook = item => mapValues(item, v => v.toUpperCase()); localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify([ {id: `fakeID1`, flushAfter: Date.now() - 60000, payload: { @@ -714,8 +713,8 @@ describe(`RequestBatcher`, function() { batcher.enqueue({foo: `bar`}); batcher.start(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([ {Hello: `WORLD`}, {foo: `BAR`}, {event: `orphaned event 1`}, // did not get uppercased @@ -729,7 +728,7 @@ describe(`RequestBatcher`, function() { it(`ignores and overwrites malformed localStorage entries`, function() { localStorage.setItem(LOCALSTORAGE_KEY, `just some garbage {{{`); batcher.start(); - expect(batcher.options.sendRequestFunc).not.to.have.been.called; + expect(batcher.sendRequest).not.to.have.been.called; // should clear and overwrite garbage localStorage when enqueueing batcher.enqueue({foo: `bar`}); @@ -740,8 +739,8 @@ describe(`RequestBatcher`, function() { ]); clock.tick(DEFAULT_FLUSH_INTERVAL); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - expect(batcher.options.sendRequestFunc.args[0][0]).to.deep.equal([ + expect(batcher.sendRequest).to.have.been.calledOnce; + expect(batcher.sendRequest.args[0][0]).to.deep.equal([ {foo: `bar`}, {baz: `quux`}, ]); @@ -762,8 +761,8 @@ describe(`RequestBatcher`, function() { expect(JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY))).to.have.lengthOf(3); batcher.start(); - expect(batcher.options.sendRequestFunc).to.have.been.calledOnce; - const payload = batcher.options.sendRequestFunc.args[0][0]; + expect(batcher.sendRequest).to.have.been.calledOnce; + const payload = batcher.sendRequest.args[0][0]; expect(payload).to.have.lengthOf(2); expect(payload[0]).to.have.property(`event`, `orphaned event 1`); expect(payload[0]).to.have.nested.include({'properties.foo': `bar`}); From 4faa34c4d939b9fb4dbd8c637aae82b5cb9ed9ba Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 26 Jun 2024 19:48:58 +0000 Subject: [PATCH 18/48] fix integration tests --- tests/test.js | 102 +++++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/tests/test.js b/tests/test.js index 3193aa8e..96b4524a 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5268,7 +5268,14 @@ this.clock = sinon.useFakeTimers(); this.randomStub = sinon.stub(Math, 'random'); this.fetchStub = sinon.stub(window, 'fetch'); - this.fetchStub.returns(makeFakeFetchResponse(200)); + + // realistic successful responsse + this.fetchStub.returns(Promise.resolve(new Response(JSON.stringify({code: 200, status: 'OK'}), { + status: 200, + headers: { + 'Content-type': 'application/json' + } + }))); var recorderSrc = window.MIXPANEL_CUSTOM_LIB_URL === '../build/mixpanel.js' ? '../build/mixpanel-recorder.js' : @@ -5306,6 +5313,9 @@ this.clock.restore(); this.randomStub.restore(); this.fetchStub.restore(); + if (this.responseBlobStub) { + this.responseBlobStub.restore(); + } var scriptEl = this.getRecorderScript(); if (scriptEl) { @@ -5313,7 +5323,19 @@ } delete window['__mp_recorder']; } - }) + }); + + function validateAndGetUrlParams(fetchStubCall) { + var calledURL = fetchStubCall.args[0]; + ok(calledURL.startsWith("https://api-js.mixpanel.com/record/")); + + var paramsStr = calledURL.split('?')[1]; + var params = new URLSearchParams(paramsStr); + same(params.get('distinct_id'), mixpanel.recordertest.get_distinct_id()); + same(params.get('$device_id'), mixpanel.recordertest.get_property('$device_id')); + + return params; + } asyncTest('adds script tag when sampled', 2, function () { this.randomStub.returns(0.02); @@ -5501,15 +5523,17 @@ }, this), 2); } }, this)); - same(this.getRecordRequests().length, 1, 'no /record calls made after user has opted out.'); - mixpanel.recordertest.stop_session_recording(); }); }); - asyncTest('retries record request after a 500', 12, function () { + asyncTest('retries record request after a 500', 14, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null); + + // fake the fetch / response promises since we're testing callback logic + this.responseBlobStub = sinon.stub(window.Response.prototype, 'blob'); + this.responseBlobStub.returns(fakePromiseWrap(new Blob())); this.fetchStub.onFirstCall() .returns(makeFakeFetchResponse(200)) .onSecondCall() @@ -5520,32 +5544,31 @@ this.clock.tick(10 * 1000) same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); - var callArgs = this.fetchStub.getCall(0).args; - same(callArgs[0], "https://api-js.mixpanel.com/record/"); - var payload = JSON.parse(callArgs[1].body); - same(payload.distinct_id, mixpanel.recordertest.get_distinct_id()); - same(payload.$device_id, mixpanel.recordertest.get_property('$device_id')); - ok(payload.events.length > 0); - + validateAndGetUrlParams(this.fetchStub.getCall(0)); + simulateMouseClick(document.body); this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); - + validateAndGetUrlParams(this.fetchStub.getCall(1)); + this.clock.tick(20 * 2000); same(this.fetchStub.getCalls().length, 3, 'record request is retried after a 500'); - callArgs = this.fetchStub.getCalls()[2].args; - payload = JSON.parse(callArgs[1].body); - ok(callArgs[0] === "https://api-js.mixpanel.com/record/") - ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); - ok(payload.events.length > 0); + validateAndGetUrlParams(this.fetchStub.getCall(2)) + mixpanel.recordertest.stop_session_recording(); }); }); - asyncTest('halves batch size and retries record request after a 413', 17, function () { + asyncTest('halves batch size and retries record request after a 413', 21, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null); + + this.randomStub.restore(); // restore the random stub after script is loaded for batcher uuid dedupe + this.blobConstructorSpy = sinon.spy(window, 'Blob') + this.responseBlobStub = sinon.stub(window.Response.prototype, 'blob'); + this.responseBlobStub.returns(fakePromiseWrap(new Blob())); + this.fetchStub.onCall(0) .returns(makeFakeFetchResponse(200)) .onCall(1) @@ -5559,40 +5582,33 @@ simulateMouseClick(document.body); this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); + + validateAndGetUrlParams(this.fetchStub.getCall(0)); - var callArgs = this.fetchStub.getCall(0).args; - same(callArgs[0], "https://api-js.mixpanel.com/record/"); - var payload = JSON.parse(callArgs[1].body); - same(payload.distinct_id, mixpanel.recordertest.get_distinct_id()); - same(payload.$device_id, mixpanel.recordertest.get_property('$device_id')); - ok(payload.events.length > 0); - - this.randomStub.restore(); for (var _i = 0; _i < 1000; _i++) { simulateMouseClick(document.body); } + this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); - callArgs = this.fetchStub.getCall(1).args; - payload = JSON.parse(callArgs[1].body); - same(payload.events.length, 1000); + validateAndGetUrlParams(this.fetchStub.getCall(1)); - this.clock.tick(10 * 1000); - same(this.fetchStub.getCalls().length, 3, 'record request is retried after a 413'); - callArgs = this.fetchStub.getCall(2).args; - payload = JSON.parse(callArgs[1].body); - ok(callArgs[0] === "https://api-js.mixpanel.com/record/") - ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); - same(payload.events.length, 500, 'batch request was halved'); + var events = JSON.parse(this.blobConstructorSpy.lastCall.args[0][0]) + same(events.length, 1000); this.clock.tick(10 * 1000); - same(this.fetchStub.getCalls().length, 4, 'remaining requests in the queue are flushed'); - callArgs = this.fetchStub.getCall(3).args; - payload = JSON.parse(callArgs[1].body); - ok(callArgs[0] === "https://api-js.mixpanel.com/record/") - ok(payload.distinct_id === mixpanel.recordertest.get_distinct_id()); - same(payload.events.length, 500, 'batch request was halved'); + same(this.fetchStub.getCalls().length, 4, 'record request is retried after a 413 and subsequently flushes the rest of events'); + validateAndGetUrlParams(this.fetchStub.getCall(2)); + validateAndGetUrlParams(this.fetchStub.getCall(3)); + + var numBlobCalls = this.blobConstructorSpy.getCalls().length + + // need to look at last 2 calls because rrweb also uses Blob while tracking + same(JSON.parse(this.blobConstructorSpy.getCall(numBlobCalls - 2).args[0][0]).length, 500, 'first batch request was halved'); + same(JSON.parse(this.blobConstructorSpy.getCall(numBlobCalls - 1).args[0][0]).length, 500, 'second batch request was halved'); + this.clock.tick(20 * 1000); + same(this.fetchStub.getCalls().length, 4, 'all events are flushed, no more requests are made'); mixpanel.recordertest.stop_session_recording(); }); }); From fb2b3616ffbb64d06ec8c017e4579df4b9cde407 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 3 Jul 2024 16:48:27 +0000 Subject: [PATCH 19/48] don't change public API --- src/mixpanel-core.js | 6 +++--- src/recorder/index.js | 2 +- src/request-batcher.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 2e2fbfdb..536e92b9 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -652,11 +652,11 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { if (verbose_mode) { var response = {}; try { - response['responseBody'] = _.JSONDecode(req.responseText); + response = _.JSONDecode(req.responseText); } catch (e) { lib.report_error(e); if (options.ignore_json_errors) { - response['responseBody'] = req.responseText; + response = req.responseText; } else { return; } @@ -680,7 +680,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: req.status, error: error, retryAfter: req.responseHeaders['Retry-After']}); + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: req['responseHeaders']['Retry-After']}); } else { callback(0); } diff --git a/src/recorder/index.js b/src/recorder/index.js index 9d7f785c..7ca19795 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -138,7 +138,7 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) 'body': reqBody, }).then(function (response) { response.json().then(function (responseBody) { - callback({status: response.status, responseBody: responseBody, retryAfter: response.headers.get('Retry-After')}); + callback({status: 0, httpStatusCode: response.status, responseBody: responseBody, retryAfter: response.headers.get('Retry-After')}); }).catch(function (error) { callback({error: error}); }); diff --git a/src/request-batcher.js b/src/request-batcher.js index 338454a4..aece133f 100644 --- a/src/request-batcher.js +++ b/src/request-batcher.js @@ -18,7 +18,7 @@ var RequestBatcher = function(storageKey, options) { this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), storage: options.storage, - usePersistence: options.usePersistence, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -194,7 +194,7 @@ RequestBatcher.prototype.flush = function(options) { this.flush(); } else if ( _.isObject(res) && - (res.status >= 500 || res.status === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; @@ -204,7 +204,7 @@ RequestBatcher.prototype.flush = function(options) { retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.status === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); From a03fd5383cfa9e40d42a437b3ce7ff18e696242b Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 3 Jul 2024 16:53:30 +0000 Subject: [PATCH 20/48] don't dupe l188 --- src/recorder/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 7ca19795..a911324e 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -179,7 +179,6 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da } if (CompressionStream) { - reqParams['format'] = 'gzip'; var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); new Response(gzipStream) From b0d2071890a20f88fe47823b596757e1f9deeca6 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 3 Jul 2024 16:55:37 +0000 Subject: [PATCH 21/48] pdate batcher unit tests --- tests/unit/request-batcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/request-batcher.js b/tests/unit/request-batcher.js index 83d5bf4d..77e02e64 100644 --- a/tests/unit/request-batcher.js +++ b/tests/unit/request-batcher.js @@ -27,11 +27,11 @@ describe(`RequestBatcher`, function() { return JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY)); } - function sendResponse(status, {error, retryAfter} = {}) { + function sendResponse(httpStatusCode, {error, retryAfter} = {}) { // respond to last request sent const requestIndex = batcher.sendRequest.args.length - 1; batcher.sendRequest.args[requestIndex][2]({ - status, + httpStatusCode, retryAfter, error, }); From 4f8fd6cc270520cdeff31151b542e934fc4f7a7b Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 3 Jul 2024 16:56:30 +0000 Subject: [PATCH 22/48] newl --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0cc9863e..ba7e8679 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,4 +20,4 @@ "port": 9229 } ] -} \ No newline at end of file +} From 97fa54ffdc4b5f11e7b26cbd04ce5ded433db119 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 11 Jun 2024 22:39:54 +0000 Subject: [PATCH 23/48] add option for sync cjs bundle --- build.sh | 1 + src/loaders/bundle-loaders.js | 12 ++++++++++++ src/loaders/loader-module-full.js | 9 +++++++++ src/loaders/loader-module.js | 3 ++- src/mixpanel-core.js | 13 ++++++------- 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 src/loaders/bundle-loaders.js create mode 100644 src/loaders/loader-module-full.js diff --git a/build.sh b/build.sh index c7d96128..a5ba8e4c 100755 --- a/build.sh +++ b/build.sh @@ -27,6 +27,7 @@ if [ ! -z "$FULL" ]; then echo 'Building module bundles' npx rollup -i src/loaders/loader-module.js -f amd -o build/mixpanel.amd.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f cjs -o build/mixpanel.cjs.js -c rollup.config.js + npx rollup -i src/loaders/loader-module-full.js -f cjs -o build/mixpanel-full.cjs.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f umd -o build/mixpanel.umd.js -n mixpanel -c rollup.config.js echo 'Bundling module-loader test runners' diff --git a/src/loaders/bundle-loaders.js b/src/loaders/bundle-loaders.js new file mode 100644 index 00000000..22e301a2 --- /dev/null +++ b/src/loaders/bundle-loaders.js @@ -0,0 +1,12 @@ +export function loadAsync (src, onload) { + var scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + scriptEl.async = true; + scriptEl.onload = onload; + scriptEl.src = src; + document.head.appendChild(scriptEl); +} + +export function loadNoop (_src, onload) { + onload(); +} diff --git a/src/loaders/loader-module-full.js b/src/loaders/loader-module-full.js new file mode 100644 index 00000000..ca8e6130 --- /dev/null +++ b/src/loaders/loader-module-full.js @@ -0,0 +1,9 @@ +/* eslint camelcase: "off" */ +import '../recorder/index.js'; + +import { init_as_module } from '../mixpanel-core'; +import {loadNoop} from './bundle-loaders'; + +var mixpanel = init_as_module(loadNoop); + +export default mixpanel; diff --git a/src/loaders/loader-module.js b/src/loaders/loader-module.js index 1516bdf4..f0f0f323 100644 --- a/src/loaders/loader-module.js +++ b/src/loaders/loader-module.js @@ -1,6 +1,7 @@ /* eslint camelcase: "off" */ import { init_as_module } from '../mixpanel-core'; +import {loadAsync} from './bundle-loaders'; -var mixpanel = init_as_module(); +var mixpanel = init_as_module(loadAsync); export default mixpanel; diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 4de12093..95f23543 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -47,6 +47,9 @@ Globals should be all caps */ var init_type; // MODULE or SNIPPET loader +var load_recorder = function(_onload) { + throw new Error('The recorder is not available in this version of the Mixpanel library.'); +}; var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -376,12 +379,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(functi }, this); if (_.isUndefined(window['__mp_recorder'])) { - var scriptEl = document.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document.head.appendChild(scriptEl); + load_recorder(handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -2281,7 +2279,8 @@ export function init_from_snippet() { add_dom_loaded_handler(); } -export function init_as_module() { +export function init_as_module(recorder_loader) { + load_recorder = recorder_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); From 92a1df3f46bdb0dc44ddce4eb42672c85b50379c Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 11 Jun 2024 22:47:03 +0000 Subject: [PATCH 24/48] fix --- src/mixpanel-core.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 95f23543..cdb69598 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -47,7 +47,9 @@ Globals should be all caps */ var init_type; // MODULE or SNIPPET loader -var load_recorder = function(_onload) { +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_recorder = function(_src, _onload) { throw new Error('The recorder is not available in this version of the Mixpanel library.'); }; var mixpanel_master; // main mixpanel instance / object @@ -379,7 +381,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(functi }, this); if (_.isUndefined(window['__mp_recorder'])) { - load_recorder(handleLoadedRecorder); + load_recorder(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -2279,8 +2281,8 @@ export function init_from_snippet() { add_dom_loaded_handler(); } -export function init_as_module(recorder_loader) { - load_recorder = recorder_loader; +export function init_as_module(bundle_loader) { + load_recorder = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); From 167d6f90b80a73f75f78e3f9496b432d556d3269 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 12 Jun 2024 22:32:10 +0000 Subject: [PATCH 25/48] fix snippet --- src/loaders/loader-globals.js | 3 ++- src/mixpanel-core.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/loaders/loader-globals.js b/src/loaders/loader-globals.js index e64b4b9b..4fe70f56 100644 --- a/src/loaders/loader-globals.js +++ b/src/loaders/loader-globals.js @@ -1,4 +1,5 @@ /* eslint camelcase: "off" */ import { init_from_snippet } from '../mixpanel-core'; +import {loadAsync} from './bundle-loaders'; -init_from_snippet(); +init_from_snippet(loadAsync); diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index cdb69598..f0e0d0db 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -2241,7 +2241,8 @@ var add_dom_loaded_handler = function() { _.register_event(window, 'load', dom_loaded_handler, true); }; -export function init_from_snippet() { +export function init_from_snippet(bundle_loader) { + load_recorder = bundle_loader; init_type = INIT_SNIPPET; mixpanel_master = window[PRIMARY_INSTANCE_NAME]; From dd608bc46049acfa5ca46e20d50a13eb05d25f3a Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Mon, 24 Jun 2024 23:15:42 +0000 Subject: [PATCH 26/48] make bundled the default --- README.md | 6 ++++++ build.sh | 2 +- src/loaders/loader-module-full.js | 9 --------- src/loaders/loader-module-main.js | 9 +++++++++ src/loaders/loader-module.js | 4 ++-- src/mixpanel-core.js | 11 ++++++----- 6 files changed, 24 insertions(+), 17 deletions(-) delete mode 100644 src/loaders/loader-module-full.js create mode 100644 src/loaders/loader-module-main.js diff --git a/README.md b/README.md index 693f0cf3..bfd9e927 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ mixpanel.init("YOUR_TOKEN"); mixpanel.track("An event"); ``` +NOTE: the default `mixpanel-browser` bundle includes a bundled `mixpanel-recorder` SDK. If you would like to reduce bundle size by loading the `mixpanel-recorder` async or if you do not us Session Replay, you may load just the main build: + +```javascript +var mixpanel = require('mixpanel-browser/dist/mixpanel-main.cjs') +``` + ## Alternative installation via Bower `mixpanel-js` is also available via front-end package manager [Bower](http://bower.io/). After installing Bower, fetch into your project's `bower_components` dir with: ```sh diff --git a/build.sh b/build.sh index a5ba8e4c..ad807752 100755 --- a/build.sh +++ b/build.sh @@ -27,7 +27,7 @@ if [ ! -z "$FULL" ]; then echo 'Building module bundles' npx rollup -i src/loaders/loader-module.js -f amd -o build/mixpanel.amd.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f cjs -o build/mixpanel.cjs.js -c rollup.config.js - npx rollup -i src/loaders/loader-module-full.js -f cjs -o build/mixpanel-full.cjs.js -c rollup.config.js + npx rollup -i src/loaders/loader-module-main.js -f cjs -o build/mixpanel-main.cjs.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f umd -o build/mixpanel.umd.js -n mixpanel -c rollup.config.js echo 'Bundling module-loader test runners' diff --git a/src/loaders/loader-module-full.js b/src/loaders/loader-module-full.js deleted file mode 100644 index ca8e6130..00000000 --- a/src/loaders/loader-module-full.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint camelcase: "off" */ -import '../recorder/index.js'; - -import { init_as_module } from '../mixpanel-core'; -import {loadNoop} from './bundle-loaders'; - -var mixpanel = init_as_module(loadNoop); - -export default mixpanel; diff --git a/src/loaders/loader-module-main.js b/src/loaders/loader-module-main.js new file mode 100644 index 00000000..c0bbbf81 --- /dev/null +++ b/src/loaders/loader-module-main.js @@ -0,0 +1,9 @@ +/* eslint camelcase: "off" */ +import '../recorder/index.js'; + +import {init_as_module} from '../mixpanel-core.js'; +import {loadAsync} from './bundle-loaders.js'; + +var mixpanel = init_as_module(loadAsync); + +export default mixpanel; diff --git a/src/loaders/loader-module.js b/src/loaders/loader-module.js index f0f0f323..2c1fca67 100644 --- a/src/loaders/loader-module.js +++ b/src/loaders/loader-module.js @@ -1,7 +1,7 @@ /* eslint camelcase: "off" */ import { init_as_module } from '../mixpanel-core'; -import {loadAsync} from './bundle-loaders'; +import { loadNoop } from './bundle-loaders'; -var mixpanel = init_as_module(loadAsync); +var mixpanel = init_as_module(loadNoop); export default mixpanel; diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index f0e0d0db..9b7ee46c 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -49,9 +49,10 @@ Globals should be all caps var init_type; // MODULE or SNIPPET loader // allow bundlers to specify how extra code (recorder bundle) should be loaded // eslint-disable-next-line no-unused-vars -var load_recorder = function(_src, _onload) { - throw new Error('The recorder is not available in this version of the Mixpanel library.'); +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); }; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -381,7 +382,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(functi }, this); if (_.isUndefined(window['__mp_recorder'])) { - load_recorder(this.get_config('recorder_src'), handleLoadedRecorder); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -2242,7 +2243,7 @@ var add_dom_loaded_handler = function() { }; export function init_from_snippet(bundle_loader) { - load_recorder = bundle_loader; + load_extra_bundle = bundle_loader; init_type = INIT_SNIPPET; mixpanel_master = window[PRIMARY_INSTANCE_NAME]; @@ -2283,7 +2284,7 @@ export function init_from_snippet(bundle_loader) { } export function init_as_module(bundle_loader) { - load_recorder = bundle_loader; + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); From eda9d36fe22146b7d7a337b258470a3e166ba181 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 3 Jul 2024 19:37:49 +0000 Subject: [PATCH 27/48] make bundled the default, add error bundle, fix tests / es5 --- README.md | 8 +- build.sh | 1 + src/loaders/bundle-loaders.js | 9 +++ src/loaders/loader-module-bundle-async.js | 7 ++ src/loaders/loader-module-main.js | 6 +- src/loaders/loader-module.js | 2 + tests/test.js | 91 +++++++++++++++-------- 7 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 src/loaders/loader-module-bundle-async.js diff --git a/README.md b/README.md index bfd9e927..b746818e 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,18 @@ mixpanel.init("YOUR_TOKEN"); mixpanel.track("An event"); ``` -NOTE: the default `mixpanel-browser` bundle includes a bundled `mixpanel-recorder` SDK. If you would like to reduce bundle size by loading the `mixpanel-recorder` async or if you do not us Session Replay, you may load just the main build: +NOTE: the default `mixpanel-browser` bundle includes a bundled `mixpanel-recorder` SDK. We provide the following options to exclude `mixpanel-recorder` if you do not intend to use session replay or want to reduce bundle size: +To load the main SDK with no option of session recording: ```javascript var mixpanel = require('mixpanel-browser/dist/mixpanel-main.cjs') ``` +To load the main SDK and optionally load session recording bundle asynchronously (via script tag): +```javascript +var mixpanel = require('mixpanel-browser/dist/mixpanel-bundle-async.cjs') +``` + ## Alternative installation via Bower `mixpanel-js` is also available via front-end package manager [Bower](http://bower.io/). After installing Bower, fetch into your project's `bower_components` dir with: ```sh diff --git a/build.sh b/build.sh index ad807752..1ae3d16c 100755 --- a/build.sh +++ b/build.sh @@ -28,6 +28,7 @@ if [ ! -z "$FULL" ]; then npx rollup -i src/loaders/loader-module.js -f amd -o build/mixpanel.amd.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f cjs -o build/mixpanel.cjs.js -c rollup.config.js npx rollup -i src/loaders/loader-module-main.js -f cjs -o build/mixpanel-main.cjs.js -c rollup.config.js + npx rollup -i src/loaders/loader-module-bundle-async.js -f cjs -o build/mixpanel-bundle-async.cjs.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f umd -o build/mixpanel.umd.js -n mixpanel -c rollup.config.js echo 'Bundling module-loader test runners' diff --git a/src/loaders/bundle-loaders.js b/src/loaders/bundle-loaders.js index 22e301a2..6e7db0d2 100644 --- a/src/loaders/bundle-loaders.js +++ b/src/loaders/bundle-loaders.js @@ -1,3 +1,5 @@ +// For loading separate bundles asynchronously via script tag +// so that we don't load them until they are needed at runtime. export function loadAsync (src, onload) { var scriptEl = document.createElement('script'); scriptEl.type = 'text/javascript'; @@ -7,6 +9,13 @@ export function loadAsync (src, onload) { document.head.appendChild(scriptEl); } +// For builds that have everything in one bundle, no extra work. export function loadNoop (_src, onload) { onload(); } + +// For builds that do NOT want any extra bundles (e.g. session recorder) +// and just the main SDK, throw an error when trying to load a separate bundle. +export function loadThrowError (src, _onload) { + throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); +} diff --git a/src/loaders/loader-module-bundle-async.js b/src/loaders/loader-module-bundle-async.js new file mode 100644 index 00000000..87918546 --- /dev/null +++ b/src/loaders/loader-module-bundle-async.js @@ -0,0 +1,7 @@ +/* eslint camelcase: "off" */ +import {init_as_module} from '../mixpanel-core.js'; +import {loadAsync} from './bundle-loaders.js'; + +var mixpanel = init_as_module(loadAsync); + +export default mixpanel; diff --git a/src/loaders/loader-module-main.js b/src/loaders/loader-module-main.js index c0bbbf81..8539ddb8 100644 --- a/src/loaders/loader-module-main.js +++ b/src/loaders/loader-module-main.js @@ -1,9 +1,7 @@ /* eslint camelcase: "off" */ -import '../recorder/index.js'; - import {init_as_module} from '../mixpanel-core.js'; -import {loadAsync} from './bundle-loaders.js'; +import {loadThrowError} from './bundle-loaders.js'; -var mixpanel = init_as_module(loadAsync); +var mixpanel = init_as_module(loadThrowError); export default mixpanel; diff --git a/src/loaders/loader-module.js b/src/loaders/loader-module.js index 2c1fca67..50bedc8c 100644 --- a/src/loaders/loader-module.js +++ b/src/loaders/loader-module.js @@ -1,4 +1,6 @@ /* eslint camelcase: "off" */ +import '../../build/mixpanel-recorder'; + import { init_as_module } from '../mixpanel-core'; import { loadNoop } from './bundle-loaders'; diff --git a/tests/test.js b/tests/test.js index 58394e15..fedf8287 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5228,7 +5228,8 @@ }); } - + // module tests have the recorder bundled in already, so don't need to test certain things + const IS_RECORDER_BUNDLED = Boolean(window['__mp_recorder']); if (window.MutationObserver) { module('recorder', { setup: function () { @@ -5255,15 +5256,33 @@ return document.querySelector('script[src="' + recorderSrc + '"]'); } - same(this.getRecorderScript(), null); + this.assertRecorderScript = function (exists) { + if (IS_RECORDER_BUNDLED) { + ok(true, 'recorder is bundled, so we dont need to check the script') + } else if (exists) { + ok(this.getRecorderScript() !== null, 'recorder script should exist'); + } else { + same(this.getRecorderScript(), null, 'recorder script should not exist') + } + } - this.afterRecorderLoaded = function (testFn) { - testFn = testFn.bind(this); - this.getRecorderScript().addEventListener('load', function() { + this.assertRecorderScript(false); + + if (IS_RECORDER_BUNDLED) { + this.afterRecorderLoaded = function (testFn) { + testFn = testFn.bind(this) testFn(); start(); - }); - }; + } + } else { + this.afterRecorderLoaded = function (testFn) { + testFn = testFn.bind(this); + this.getRecorderScript().addEventListener('load', function() { + testFn(); + start(); + }); + }; + } }, teardown: function () { if (mixpanel.recordertest) { @@ -5279,29 +5298,34 @@ if (scriptEl) { scriptEl.parentNode.removeChild(scriptEl); } - delete window['__mp_recorder']; + + if (!IS_RECORDER_BUNDLED) { + delete window['__mp_recorder']; + } } }) - asyncTest('adds script tag when sampled', 2, function () { - this.randomStub.returns(0.02); - this.initMixpanelRecorder({record_sessions_percent: 2}); - ok(this.getRecorderScript() !== null); - this.afterRecorderLoaded(function() { - mixpanel.recordertest.stop_session_recording(); - }) - }); - - test('does not add script tag when not sampled', 2, function () { - this.randomStub.returns(0.02); - this.initMixpanelRecorder({record_sessions_percent: 1}); - ok(this.getRecorderScript() === null); - }); + if (!IS_RECORDER_BUNDLED) { + asyncTest('adds script tag when sampled', 2, function () { + this.randomStub.returns(0.02); + this.initMixpanelRecorder({record_sessions_percent: 2}); + this.assertRecorderScript(true); + this.afterRecorderLoaded(function() { + mixpanel.recordertest.stop_session_recording(); + }) + }); + + test('does not add script tag when not sampled', 2, function () { + this.randomStub.returns(0.02); + this.initMixpanelRecorder({record_sessions_percent: 1}); + this.assertRecorderScript(false); + }); + } asyncTest('sends recording payload to server', 12, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); - ok(this.getRecorderScript() !== null, "recorder script loaded"); + this.assertRecorderScript(true); this.afterRecorderLoaded.call(this, function () { simulateMouseClick(document.body); @@ -5359,7 +5383,7 @@ asyncTest('can manually start a session recording', 5, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 1}); - ok(this.getRecorderScript() === null); + this.assertRecorderScript(false); this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 0, 'no /record call has been made since the user did not fall into the sample.'); @@ -5385,7 +5409,7 @@ asyncTest('can manually stop a session recording', function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); - ok(this.getRecorderScript() !== null); + this.assertRecorderScript(true); this.afterRecorderLoaded.call(this, function () { simulateMouseClick(document.body); @@ -5423,26 +5447,28 @@ }); }); - test('respects tracking opt-out when sampled', 2, function () { + test('respects tracking opt-out when sampled', 3, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10, window: {navigator: {doNotTrack: '1'}}}); - ok(this.getRecorderScript() === null); + this.assertRecorderScript(false); + same(Object.keys(mixpanel.recordertest.get_session_recording_properties()).length, 0, 'no recording is taking place') }); - test('respects tracking opt-out when manually triggered', 3, function () { + test('respects tracking opt-out when manually triggered', 4, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10, window: {navigator: {doNotTrack: '1'}}}); - ok(this.getRecorderScript() === null); + this.assertRecorderScript(false); mixpanel.recordertest.start_session_recording(); - ok(this.getRecorderScript() === null); + this.assertRecorderScript(false); + same(Object.keys(mixpanel.recordertest.get_session_recording_properties()).length, 0, 'no recording is taking place') }); - asyncTest('respects tracking opt-out after recording started', function () { + asyncTest('respects tracking opt-out after recording started', 5, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); - ok(this.getRecorderScript() !== null); + this.assertRecorderScript(true); this.afterRecorderLoaded.call(this, function () { simulateMouseClick(document.body); @@ -5464,6 +5490,7 @@ this.clock.tick(10 * 1000); realSetTimeout(_.bind(function() { same(this.fetchStub.getCalls().length, 1, 'no /record calls made after user has opted out.'); + same(Object.keys(mixpanel.recordertest.get_session_recording_properties()).length, 0, 'no recording is taking place') mixpanel.recordertest.stop_session_recording(); done(); }, this), 2); From 4c3191bfa749e812237c09bfcfdbe578753a26aa Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Mon, 8 Jul 2024 15:43:12 -0500 Subject: [PATCH 28/48] Update src/mixpanel-core.js Co-authored-by: teddddd --- src/mixpanel-core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 536e92b9..f3b90da3 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -650,7 +650,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { if (req.status === 200) { if (callback) { if (verbose_mode) { - var response = {}; + var response; try { response = _.JSONDecode(req.responseText); } catch (e) { From c412635ac7f260c97ff4abb91439fc614d04ac0b Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Mon, 8 Jul 2024 15:43:24 -0500 Subject: [PATCH 29/48] Update src/recorder/index.js Co-authored-by: teddddd --- src/recorder/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index a911324e..06ab41cc 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -138,7 +138,12 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) 'body': reqBody, }).then(function (response) { response.json().then(function (responseBody) { - callback({status: 0, httpStatusCode: response.status, responseBody: responseBody, retryAfter: response.headers.get('Retry-After')}); + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); }).catch(function (error) { callback({error: error}); }); From ccb36cbbc83b8117238de03f970bd3b76a2fcde2 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Mon, 8 Jul 2024 21:00:00 +0000 Subject: [PATCH 30/48] gaurd for responseHeaders --- src/mixpanel-core.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index f3b90da3..5159f6d4 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -680,7 +680,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: req['responseHeaders']['Retry-After']}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } From ac53d032c47b333876261a7f2437678d4fbaff20 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 10 Jul 2024 13:52:22 -0500 Subject: [PATCH 31/48] Update src/recorder/index.js Co-authored-by: teddddd --- src/recorder/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 06ab41cc..9bea0dec 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -1,6 +1,6 @@ import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js'; -import { MAX_RECORDING_MS, console_with_prefix, _, } from '../utils'; // eslint-disable-line camelcase +import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase import { addOptOutCheckMixpanelLib } from '../gdpr-utils'; import { RequestBatcher } from '../request-batcher'; From 03752b12f80ed4b711525940636c208c1c6f497d Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 10 Jul 2024 13:52:37 -0500 Subject: [PATCH 32/48] Update src/recorder/index.js Co-authored-by: teddddd --- src/recorder/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 9bea0dec..8d433eda 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -11,7 +11,7 @@ var RECORDER_BATCHER_LIB_CONFIG = { 'batch_size': 1000, 'batch_flush_interval_ms': 10 * 1000, 'batch_request_timeout_ms': 90 * 1000, - 'batch_autostart': true, + 'batch_autostart': true }; var MixpanelRecorder = function(mixpanelInstance) { From 6f1d787c87b14d2832f8a195f0500bab870f3310 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 10 Jul 2024 13:52:48 -0500 Subject: [PATCH 33/48] Update src/recorder/index.js Co-authored-by: teddddd --- src/recorder/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index 8d433eda..ccf67f9f 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -40,7 +40,7 @@ MixpanelRecorder.prototype._initBatcher = function () { sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), errorReporter: _.bind(this.reportError, this), flushOnlyOnInterval: true, - usePersistence: false, + usePersistence: false }); }; From ea4fcb397999e41f089e86baab5eecf7dc139915 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Thu, 11 Jul 2024 23:55:29 +0000 Subject: [PATCH 34/48] lint --- src/loaders/bundle-loaders.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/loaders/bundle-loaders.js b/src/loaders/bundle-loaders.js index 6e7db0d2..5763ef9d 100644 --- a/src/loaders/bundle-loaders.js +++ b/src/loaders/bundle-loaders.js @@ -16,6 +16,7 @@ export function loadNoop (_src, onload) { // For builds that do NOT want any extra bundles (e.g. session recorder) // and just the main SDK, throw an error when trying to load a separate bundle. +// eslint-disable-next-line no-unused-vars export function loadThrowError (src, _onload) { throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); } From de9b81babf8e58ab292a278f13c531ba299395e4 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 12 Jul 2024 01:49:50 +0000 Subject: [PATCH 35/48] fix tests --- src/loaders/loader-module.js | 2 +- tests/unit/loader.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/loaders/loader-module.js b/src/loaders/loader-module.js index 50bedc8c..630cf697 100644 --- a/src/loaders/loader-module.js +++ b/src/loaders/loader-module.js @@ -1,5 +1,5 @@ /* eslint camelcase: "off" */ -import '../../build/mixpanel-recorder'; +import '../../dist/mixpanel-recorder'; import { init_as_module } from '../mixpanel-core'; import { loadNoop } from './bundle-loaders'; diff --git a/tests/unit/loader.js b/tests/unit/loader.js index 386d4149..228d7b06 100644 --- a/tests/unit/loader.js +++ b/tests/unit/loader.js @@ -5,9 +5,17 @@ * currently not supported in the browser lib). */ -import mixpanel from '../../src/loaders/loader-module'; +import jsDomSetup from './jsdom-setup'; describe(`Module-based loader in Node env`, function() { + let mixpanel; + jsDomSetup({ + reImportModules: [`../../src/loaders/loader-module`], + beforeCallback: function(modules) { + mixpanel = modules[0]; + } + }); + it(`supports init() with options`, function(done) { mixpanel.init(`test-token`, { debug: true, From 614dd794a83862bb4534076df71dede10dd50bbf Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 12 Jul 2024 02:41:34 +0000 Subject: [PATCH 36/48] inactivity as lack of user events --- src/recorder/index.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index dd573a65..f41e0839 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -1,11 +1,27 @@ import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js'; - +import {IncrementalSource, EventType} from '@rrweb/types'; import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase import { addOptOutCheckMixpanelLib } from '../gdpr-utils'; var logger = console_with_prefix('recorder'); var CompressionStream = window['CompressionStream']; +var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, +]); + +function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); +} + var MixpanelRecorder = function(mixpanelInstance) { this._mixpanel = mixpanelInstance; @@ -64,7 +80,9 @@ MixpanelRecorder.prototype.startRecording = function () { 'emit': _.bind(function (ev) { this.recEvents.push(ev); this.replayLengthMs = new Date().getTime() - this.replayStartTime; - resetIdleTimeout(); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } }, this), 'maskAllInputs': true, 'maskTextSelector': this.get_config('record_mask_text_selector'), From 3c9f2e04db0f0deed5fb14fa73bdf2eeda5c48d0 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Fri, 12 Jul 2024 02:48:21 +0000 Subject: [PATCH 37/48] options for inline images and font collect --- src/mixpanel-core.js | 2 ++ src/recorder/index.js | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/mixpanel-core.js b/src/mixpanel-core.js index 4de12093..5d03a6dc 100644 --- a/src/mixpanel-core.js +++ b/src/mixpanel-core.js @@ -140,7 +140,9 @@ var DEFAULT_CONFIG = { 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, diff --git a/src/recorder/index.js b/src/recorder/index.js index dd573a65..236c5f6b 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -66,11 +66,13 @@ MixpanelRecorder.prototype.startRecording = function () { this.replayLengthMs = new Date().getTime() - this.replayStartTime; resetIdleTimeout(); }, this), - 'maskAllInputs': true, - 'maskTextSelector': this.get_config('record_mask_text_selector'), + 'blockClass': this.get_config('record_block_class'), 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, 'maskTextClass': this.get_config('record_mask_text_class'), - 'blockClass': this.get_config('record_block_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); resetIdleTimeout(); From 5c0b5072aa73582d96e62b15b96bc78ac5da8971 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 16 Jul 2024 03:20:07 +0000 Subject: [PATCH 38/48] proper fix --- dist/mixpanel-bundle-async.cjs.js | 6332 +++++++++++++++++++++++++++++ dist/mixpanel-main.cjs.js | 6330 ++++++++++++++++++++++++++++ package.json | 1 - rollup.config.js | 4 +- src/loaders/loader-module.js | 2 +- src/recorder/index.js | 4 +- tests/unit/loader.js | 10 +- 7 files changed, 12668 insertions(+), 15 deletions(-) create mode 100644 dist/mixpanel-bundle-async.cjs.js create mode 100644 dist/mixpanel-main.cjs.js diff --git a/dist/mixpanel-bundle-async.cjs.js b/dist/mixpanel-bundle-async.cjs.js new file mode 100644 index 00000000..ba98f333 --- /dev/null +++ b/dist/mixpanel-bundle-async.cjs.js @@ -0,0 +1,6332 @@ +'use strict'; + +var Config = { + DEBUG: false, + LIB_VERSION: '2.53.0' +}; + +/* eslint camelcase: "off", eqeqeq: "off" */ + +// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file +var win; +if (typeof(window) === 'undefined') { + var loc = { + hostname: '' + }; + win = { + navigator: { userAgent: '' }, + document: { + location: loc, + referrer: '' + }, + screen: { width: 0, height: 0 }, + location: loc + }; +} else { + win = window; +} + +// Maximum allowed session recording length +var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours + +/* + * Saved references to long variable names, so that closure compiler can + * minimize file size. + */ + +var ArrayProto = Array.prototype, + FuncProto = Function.prototype, + ObjProto = Object.prototype, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty, + windowConsole = win.console, + navigator = win.navigator, + document$1 = win.document, + windowOpera = win.opera, + screen = win.screen, + userAgent = navigator.userAgent; + +var nativeBind = FuncProto.bind, + nativeForEach = ArrayProto.forEach, + nativeIndexOf = ArrayProto.indexOf, + nativeMap = ArrayProto.map, + nativeIsArray = Array.isArray, + breaker = {}; + +var _ = { + trim: function(str) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } +}; + +// Console override +var console = { + /** @type {function(...*)} */ + log: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } + }, + /** @type {function(...*)} */ + warn: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); + try { + windowConsole.warn.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.warn(arg); + }); + } + } + }, + /** @type {function(...*)} */ + error: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + }, + /** @type {function(...*)} */ + critical: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + } +}; + +var log_func_with_prefix = function(func, prefix) { + return function() { + arguments[0] = '[' + prefix + '] ' + arguments[0]; + return func.apply(console, arguments); + }; +}; +var console_with_prefix = function(prefix) { + return { + log: log_func_with_prefix(console.log, prefix), + error: log_func_with_prefix(console.error, prefix), + critical: log_func_with_prefix(console.critical, prefix) + }; +}; + + +// UNDERSCORE +// Embed part of the Underscore Library +_.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + if (!_.isFunction(func)) { + throw new TypeError(); + } + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) { + return func.apply(context, args.concat(slice.call(arguments))); + } + var ctor = {}; + ctor.prototype = func.prototype; + var self = new ctor(); + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; +}; + +/** + * @param {*=} obj + * @param {function(...*)=} iterator + * @param {Object=} context + */ +_.each = function(obj, iterator, context) { + if (obj === null || obj === undefined) { + return; + } + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { + return; + } + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) { + return; + } + } + } + } +}; + +_.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; +}; + +_.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; +}; + +// from a comment on http://dbj.org/dbj/?p=286 +// fails on only one very rare and deliberate custom object: +// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; +_.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } +}; + +_.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); +}; + +_.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); +}; + +_.map = function(arr, callback, context) { + if (nativeMap && arr.map === nativeMap) { + return arr.map(callback, context); + } else { + var results = []; + _.each(arr, function(item) { + results.push(callback.call(context, item)); + }); + return results; + } +}; + +_.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; +}; + +_.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; +}; + +_.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; +}; + +_.includes = function(str, needle) { + return str.indexOf(needle) !== -1; +}; + +// Underscore Addons +_.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; +}; + +_.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); +}; + +_.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; +}; + +_.isUndefined = function(obj) { + return obj === void 0; +}; + +_.isString = function(obj) { + return toString.call(obj) == '[object String]'; +}; + +_.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; + +_.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; +}; + +_.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); +}; + +_.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; +}; + +_.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); +}; + +_.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); +}; + +_.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; +}; + +/* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ +_.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; +}; + +_.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; +})(); + +/** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ +_.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; +})(); + +_.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; +}; + +_.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; +}; + +_.UUID = (function() { + + // Time-based entropy + var T = function() { + var time = 1 * new Date(); // cross-browser version of Date.now() + var ticks; + if (win.performance && win.performance.now) { + ticks = win.performance.now(); + } else { + // fall back to busy loop + ticks = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (time == 1 * new Date()) { + ticks++; + } + } + return time.toString(16) + Math.floor(ticks).toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; +})(); + +// _.isBlockedUA() +// This is to block various web spiders from executing our JS and +// sending false tracking data +var BLOCKED_UA_STRS = [ + 'ahrefsbot', + 'ahrefssiteaudit', + 'baiduspider', + 'bingbot', + 'bingpreview', + 'chrome-lighthouse', + 'facebookexternal', + 'petalbot', + 'pinterest', + 'screaming frog', + 'yahoo! slurp', + 'yandexbot', + + // a whole bunch of goog-specific crawlers + // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers + 'adsbot-google', + 'apis-google', + 'duplexweb-google', + 'feedfetcher-google', + 'google favicon', + 'google web preview', + 'google-read-aloud', + 'googlebot', + 'googleweblight', + 'mediapartners-google', + 'storebot-google' +]; +_.isBlockedUA = function(ua) { + var i; + ua = ua.toLowerCase(); + for (i = 0; i < BLOCKED_UA_STRS.length; i++) { + if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { + return true; + } + } + return false; +}; + +/** + * @param {Object=} formdata + * @param {string=} arg_separator + */ +_.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); +}; + +_.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + var result = results[1]; + try { + result = decodeURIComponent(result); + } catch(err) { + console.error('Skipping decoding for malformed query param: ' + result); + } + return result.replace(/\+/g, ' '); + } +}; + + +// _.cookie +// Methods partially borrowed from quirksmode.org/js/cookies.html +_.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', + expires = '', + secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', expires = '', secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, is_cross_subdomain, domain_override) { + _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); + } +}; + +var _localStorageSupported = null; +var localStorageSupported = function(storage, forceCheck) { + if (_localStorageSupported !== null && !forceCheck) { + return _localStorageSupported; + } + + var supported = true; + try { + storage = storage || window.localStorage; + var key = '__mplss_' + cheap_guid(8), + val = 'xyz'; + storage.setItem(key, val); + if (storage.getItem(key) !== val) { + supported = false; + } + storage.removeItem(key); + } catch (err) { + supported = false; + } + + _localStorageSupported = supported; + return supported; +}; + +// _.localStorage +_.localStorage = { + is_supported: function(force_check) { + var supported = localStorageSupported(null, force_check); + if (!supported) { + console.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }, + + error: function(msg) { + console.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } +}; + +_.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...*)} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; +})(); + + +var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); + +_.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(TOKEN_MATCH_REGEX); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; +})(); + +var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; +var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; + +_.info = { + campaignParams: function(default_value) { + var kw = '', + params = {}; + _.each(CAMPAIGN_KEYWORDS, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } else if (default_value !== undefined) { + params[kwkey] = default_value; + } + }); + + return params; + }, + + clickParams: function() { + var id = '', + params = {}; + _.each(CLICK_IDS, function(idkey) { + id = _.getQueryParam(document$1.URL, idkey); + if (id.length) { + params[idkey] = id; + } + }); + + return params; + }, + + marketingParams: function() { + return _.extend(_.info.campaignParams(), _.info.clickParams()); + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'SamsungBrowser/')) { + // https://developer.samsung.com/internet/user-agent-string-format + return 'Samsung Internet'; + } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + currentUrl: function() { + return win.location.href; + }, + + properties: function(extra_props) { + if (typeof extra_props !== 'object') { + extra_props = {}; + } + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': _.info.currentUrl(), + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION, + '$insert_id': cheap_guid(), + 'time': _.timestamp() / 1000 // epoch time in seconds + }, _.strip_empty_properties(extra_props)); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) + }); + }, + + mpPageViewProperties: function() { + return _.strip_empty_properties({ + 'current_page_title': document$1.title, + 'current_domain': win.location.hostname, + 'current_url_path': win.location.pathname, + 'current_url_protocol': win.location.protocol, + 'current_url_search': win.location.search + }); + } +}; + +var cheap_guid = function(maxlen) { + var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); + return maxlen ? guid.substring(0, maxlen) : guid; +}; + +// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) +var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; +// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk +var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; +/** + * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For + * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we + * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). + * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy + * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel + * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date + * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK + * offers the 'cookie_domain' config option to set it explicitly. + * @example + * extract_domain('my.sub.example.com') + * // 'example.com' + */ +var extract_domain = function(hostname) { + var domain_regex = DOMAIN_MATCH_REGEX; + var parts = hostname.split('.'); + var tld = parts[parts.length - 1]; + if (tld.length > 4 || tld === 'com' || tld === 'org') { + domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; + } + var matches = hostname.match(domain_regex); + return matches ? matches[0] : ''; +}; + +var JSONStringify = null, JSONParse = null; +if (typeof JSON !== 'undefined') { + JSONStringify = JSON.stringify; + JSONParse = JSON.parse; +} +JSONStringify = JSONStringify || _.JSONEncode; +JSONParse = JSONParse || _.JSONDecode; + +// EXPORTS (for closure compiler) +_['toArray'] = _.toArray; +_['isObject'] = _.isObject; +_['JSONEncode'] = _.JSONEncode; +_['JSONDecode'] = _.JSONDecode; +_['isBlockedUA'] = _.isBlockedUA; +_['isEmptyObject'] = _.isEmptyObject; +_['info'] = _.info; +_['info']['device'] = _.info.device; +_['info']['browser'] = _.info.browser; +_['info']['browserVersion'] = _.info.browserVersion; +_['info']['properties'] = _.info.properties; + +/* eslint camelcase: "off" */ + +/** + * DomTracker Object + * @constructor + */ +var DomTracker = function() {}; + + +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; + +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; + +/** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback + */ +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; +}; + +/** + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; +}; + +DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; + +var logger$2 = console_with_prefix('lock'); + +/** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ +var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; +}; + +// pass in a specific pid to test contention scenarios; otherwise +// it is chosen randomly for each acquisition attempt +SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } +}; + +var logger$1 = console_with_prefix('batch'); + +/** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ +var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; +}; + +/** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ +RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ +RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; +}; + +/** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ +var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; +}; + +/** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ +RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); +}; + +// internal helper for RequestQueue.updatePayloads +var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; +}; + +/** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ +RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ +RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; +}; + +/** + * Serialize the given items array to localStorage. + */ +RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } +}; + +/** + * Clear out queues (memory and localStorage). + */ +RequestQueue.prototype.clear = function() { + this.memQueue = []; + this.storage.removeItem(this.storageKey); +}; + +// maximum interval between request retries after exponential backoff +var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + +var logger = console_with_prefix('batch'); + +/** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ +var RequestBatcher = function(storageKey, options) { + this.errorReporter = options.errorReporter; + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage + }); + + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; + + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; + + this.stopped = !this.libConfig['batch_autostart']; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; +}; + +/** + * Add one item to queue. + */ +RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.flushInterval, cb); +}; + +/** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ +RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); +}; + +/** + * Stop flushing batches. Can be restarted by calling start(). + */ +RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } +}; + +/** + * Clear out queue. + */ +RequestBatcher.prototype.clear = function() { + this.queue.clear(); +}; + +/** + * Restore batch size configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetBatchSize = function() { + this.batchSize = this.libConfig['batch_size']; +}; + +/** + * Restore flush interval time configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); +}; + +/** + * Schedule the next flush in the given number of milliseconds. + */ +RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.flushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + } +}; + +/** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ +RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var startTime = new Date().getTime(); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + res.xhr_req && + (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + var headers = res.xhr_req['responseHeaders']; + if (headers) { + var retryAfter = headers['Retry-After']; + if (retryAfter) { + retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; + } + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + this.flush(); // handle next batch if the queue isn't empty + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.stopAllBatching(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger.log('MIXPANEL REQUEST:', dataForRequest); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } +}; + +/** + * Log error to global logger and optional user-defined logger. + */ +RequestBatcher.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + if (this.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.errorReporter(msg, err); + } catch(err) { + logger.error(err); + } + } +}; + +/** + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. + */ + +/** + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + +/** Public **/ + +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} + +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type + */ +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} + +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} + +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} + +/** Private **/ + +/** + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage + */ +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} + +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} + +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} + +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; + + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); + + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; + } + + options = options || {}; + + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} + +/** + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; + + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } + + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +/* eslint camelcase: "off" */ + +/** @const */ var SET_ACTION = '$set'; +/** @const */ var SET_ONCE_ACTION = '$set_once'; +/** @const */ var UNSET_ACTION = '$unset'; +/** @const */ var ADD_ACTION = '$add'; +/** @const */ var APPEND_ACTION = '$append'; +/** @const */ var UNION_ACTION = '$union'; +/** @const */ var REMOVE_ACTION = '$remove'; +/** @const */ var DELETE_ACTION = '$delete'; + +// Common internal methods for mixpanel.people and mixpanel.group APIs. +// These methods shouldn't involve network I/O. +var apiActions = { + set_action: function(prop, to) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + } else { + $set[prop] = to; + } + + data[SET_ACTION] = $set; + return data; + }, + + unset_action: function(prop) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + return data; + }, + + set_once_action: function(prop, to) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return data; + }, + + union_action: function(list_name, values) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + return data; + }, + + append_action: function(list_name, value) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + return data; + }, + + remove_action: function(list_name, value) { + var data = {}; + var $remove = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $remove[k] = v; + } + }, this); + } else { + $remove[list_name] = value; + } + data[REMOVE_ACTION] = $remove; + return data; + }, + + delete_action: function() { + var data = {}; + data[DELETE_ACTION] = ''; + return data; + } +}; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel Group Object + * @constructor + */ +var MixpanelGroup = function() {}; + +_.extend(MixpanelGroup.prototype, apiActions); + +MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { + this._mixpanel = mixpanel_instance; + this._group_key = group_key; + this._group_id = group_id; +}; + +/** + * Set properties on a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Set properties on a group, only if they do not yet exist. + * This will not overwrite previous group property values, unlike + * group.set(). + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set_once({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, lists or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Unset properties on a group permanently. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').unset('Founded'); + * + * @param {String} prop The name of the property. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/** + * Merge a given list with a list-valued group property, excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); + * + * @param {String} list_name Name of the property. + * @param {Array} values Values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/** + * Permanently delete a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').delete(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { + // bracket notation above prevents a minification error related to reserved words + var data = this.delete_action(); + return this._send_request(data, callback); +}); + +/** + * Remove a property from a group. The value will be ignored if doesn't exist. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); + * + * @param {String} list_name Name of the property. + * @param {Object} value Value to remove from the given group property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +MixpanelGroup.prototype._send_request = function(data, callback) { + data['$group_key'] = this._group_key; + data['$group_id'] = this._group_id; + data['$token'] = this._get_config('token'); + + var date_encoded_data = _.encodeDates(data); + return this._mixpanel._track_or_batch({ + type: 'groups', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], + batcher: this._mixpanel.request_batchers.groups + }, callback); +}; + +MixpanelGroup.prototype._is_reserved_property = function(prop) { + return prop === '$group_key' || prop === '$group_id'; +}; + +MixpanelGroup.prototype._get_config = function(conf) { + return this._mixpanel.get_config(conf); +}; + +MixpanelGroup.prototype.toString = function() { + return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; +}; + +// MixpanelGroup Exports +MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; +MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; +MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; +MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; +MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; +MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel People Object + * @constructor + */ +var MixpanelPeople = function() {}; + +_.extend(MixpanelPeople.prototype, apiActions); + +MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; +}; + +/* +* Set properties on a user record. +* +* ### Usage: +* +* mixpanel.people.set('gender', 'm'); +* +* // or set multiple properties at once +* mixpanel.people.set({ +* 'Company': 'Acme', +* 'Plan': 'Premium', +* 'Upgrade date': new Date() +* }); +* // properties can be strings, integers, dates, or lists +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document.referrer); + } + + // update $set object with default people properties + data[SET_ACTION] = _.extend( + {}, + _.info.people_properties(), + data[SET_ACTION] + ); + return this._send_request(data, callback); +}); + +/* +* Set properties on a user record, only if they do not yet exist. +* This will not overwrite previous people property values, unlike +* people.set(). +* +* ### Usage: +* +* mixpanel.people.set_once('First Login Date', new Date()); +* +* // or set multiple properties at once +* mixpanel.people.set_once({ +* 'First Login Date': new Date(), +* 'Starting Plan': 'Premium' +* }); +* +* // properties can be strings, integers or dates +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/* +* Unset properties on a user record (permanently removes the properties and their values from a profile). +* +* ### Usage: +* +* mixpanel.people.unset('gender'); +* +* // or unset multiple properties at once +* mixpanel.people.unset(['gender', 'Company']); +* +* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/* +* Increment/decrement numeric people analytics properties. +* +* ### Usage: +* +* mixpanel.people.increment('page_views', 1); +* +* // or, for convenience, if you're just incrementing a counter by +* // 1, you can simply do +* mixpanel.people.increment('page_views'); +* +* // to decrement a counter, pass a negative number +* mixpanel.people.increment('credits_left', -1); +* +* // like mixpanel.people.set(), you can increment multiple +* // properties at once: +* mixpanel.people.increment({ +* counter1: 1, +* counter2: 6 +* }); +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. +* @param {Number} [by] An amount to increment the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); +}); + +/* +* Append a value to a list-valued people analytics property. +* +* ### Usage: +* +* // append a value to a list, creating it if needed +* mixpanel.people.append('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.append({ +* list1: 'bob', +* list2: 123 +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value An item to append to the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.append_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Remove a value from a list-valued people analytics property. +* +* ### Usage: +* +* mixpanel.people.remove('School', 'UCB'); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value Item to remove from the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Merge a given list with a list-valued people analytics property, +* excluding duplicate values. +* +* ### Usage: +* +* // merge a value to a list, creating it if needed +* mixpanel.people.union('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.union({ +* list1: 'bob', +* list2: 123 +* }); +* +* // like mixpanel.people.append(), you can append multiple +* // values to the same list: +* mixpanel.people.union({ +* list1: ['bob', 'billy'] +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] Value / values to merge with the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + * @deprecated + */ +MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); +}); + +/* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * @deprecated + */ +MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); +}; + +/* +* Permanently deletes the current people analytics profile from +* Mixpanel (using the current distinct_id). +* +* ### Usage: +* +* // remove the all data you have stored about the current user +* mixpanel.people.delete_user(); +* +*/ +MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); +}; + +MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; +}; + +MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + var device_id = this._mixpanel.get_property('$device_id'); + var user_id = this._mixpanel.get_property('$user_id'); + var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); + if (device_id) { + data['$device_id'] = device_id; + } + if (user_id) { + data['$user_id'] = user_id; + } + if (had_persisted_distinct_id) { + data['$had_persisted_distinct_id'] = had_persisted_distinct_id; + } + + var date_encoded_data = _.encodeDates(data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return _.truncate(date_encoded_data, 255); + } + + return this._mixpanel._track_or_batch({ + type: 'people', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], + batcher: this._mixpanel.request_batchers.people + }, callback); +}; + +MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); +}; + +MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; +}; + +// Queue up engage operations if identify hasn't been called yet. +MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (REMOVE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console.error('Invalid call to _enqueue():', data); + } +}; + +MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + _this._mixpanel['persistence'].save(); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } +}; + +// Flush queued engage operations - order does not matter, +// and there are network level race conditions anyway +MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + var _this = this; + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var append_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + $append_item = $append_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($append_item)) { + _this.append($append_item, append_callback); + } + } + } + + // same for $remove + var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { + var $remove_item; + var remove_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); + } + if (!_.isUndefined(_remove_callback)) { + _remove_callback(response, data); + } + }; + for (var j = $remove_queue.length - 1; j >= 0; j--) { + $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + $remove_item = $remove_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($remove_item)) { + _this.remove($remove_item, remove_callback); + } + } + } +}; + +MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; +}; + +// MixpanelPeople Exports +MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; +MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; +MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; +MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; +MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; +MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; +MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; +MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; +MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; +MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; +MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + +/* eslint camelcase: "off" */ + +/* + * Constants + */ +/** @const */ var SET_QUEUE_KEY = '__mps'; +/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; +/** @const */ var UNSET_QUEUE_KEY = '__mpus'; +/** @const */ var ADD_QUEUE_KEY = '__mpa'; +/** @const */ var APPEND_QUEUE_KEY = '__mpap'; +/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; +/** @const */ var UNION_QUEUE_KEY = '__mpu'; +// This key is deprecated, but we want to check for it to see whether aliasing is allowed. +/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; +/** @const */ var ALIAS_ID_KEY = '__alias'; +/** @const */ var EVENT_TIMERS_KEY = '__timers'; +/** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + REMOVE_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + EVENT_TIMERS_KEY +]; + +/** + * Mixpanel Persistence Object + * @constructor + */ +var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + if (storage_type === 'localStorage' && _.localStorage.is_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(); + this.save(); +}; + +MixpanelPersistence.prototype.properties = function() { + var p = {}; + + this.load(); + + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; +}; + +MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } +}; + +MixpanelPersistence.prototype.upgrade = function() { + var old_cookie, + old_localstorage; + + // if transferring from cookie to localStorage or vice-versa, copy existing + // super properties over to new storage mode + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } else if (this.storage === _.cookie) { + old_localstorage = _.localStorage.parse(this.name); + + _.localStorage.remove(this.name); + + if (old_localstorage) { + this.register_once(old_localstorage); + } + } +}; + +MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure, + this.cross_site, + this.cookie_domain + ); +}; + +MixpanelPersistence.prototype.load_prop = function(key) { + this.load(); + return this['props'][key]; +}; + +MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false, this.cookie_domain); + this.storage.remove(this.name, true, this.cookie_domain); +}; + +// removes the storage entry and deletes all loaded data +// forced name for tests +MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; +}; + +/** +* @param {Object} props +* @param {*=} default_value +* @param {number=} days +*/ +MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; +}; + +/** +* @param {Object} props +* @param {number=} days +*/ +MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + _.extend(this['props'], props); + this.save(); + + return true; + } + return false; +}; + +MixpanelPersistence.prototype.unregister = function(prop) { + this.load(); + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } +}; + +MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); +}; + +// EXPORTED METHOD, we test this directly. +MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); +}; + +MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); +}; + +MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cookie_domain(config['cookie_domain']); + this.set_cross_site(config['cross_site_cookie']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); +}; + +MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } else { + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { + if (cookie_domain !== this.cookie_domain) { + this.remove(); + this.cookie_domain = cookie_domain; + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_site = function(cross_site) { + if (cross_site !== this.cross_site) { + this.cross_site = cross_site; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; +}; + +MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + remove_q = this._get_or_create_queue(REMOVE_ACTION, []), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === REMOVE_QUEUE_KEY) { + remove_q.push(q_data); + this._pop_from_people_queue(APPEND_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console.log(data); + + this.save(); +}; + +MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this['props'][this._get_queue_key(queue)]; + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { + // list actions: only remove if both k+v match + // e.g. remove should not override append in a case like + // append({foo: 'bar'}); remove({foo: 'qux'}) + _.each(q, function(queued_action) { + if (queued_action[k] === v) { + delete queued_action[k]; + } + }); + } else { + delete q[k]; + } + }, this); + } +}; + +MixpanelPersistence.prototype.load_queue = function(queue) { + return this.load_prop(this._get_queue_key(queue)); +}; + +MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === REMOVE_ACTION) { + return REMOVE_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console.error('Invalid queue:', queue); + } +}; + +MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + return this['props'][key] || (this['props'][key] = default_val); +}; + +MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); +}; + +MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; +}; + +/* eslint camelcase: "off" */ + +/* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @output_file_name mixpanel-2.8.min.js +// ==/ClosureCompiler== + +/* +SIMPLE STYLE GUIDE: + +this.x === public function +this._x === internal - only use within this file +this.__x === private - only use within the class + +Globals should be all caps +*/ + +var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + +var mixpanel_master; // main mixpanel instance / object +var INIT_MODULE = 0; +var INIT_SNIPPET = 1; + +var IDENTITY_FUNC = function(x) {return x;}; +var NOOP_FUNC = function() {}; + +/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; +/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; +/** @const */ var PAYLOAD_TYPE_JSON = 'json'; +/** @const */ var DEVICE_ID_PREFIX = '$device:'; + + +/* + * Dynamic... constants? Is that an oxymoron? + */ +// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ +// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials +var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + +// IE<10 does not support cross-origin XHR's but script tags +// with defer won't block window.onload; ENQUEUE_REQUESTS +// should only be true for Opera<12 +var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + +// save reference to navigator.sendBeacon so it can be minified +var sendBeacon = null; +if (navigator['sendBeacon']) { + sendBeacon = function() { + // late reference to navigator.sendBeacon to allow patching/spying + return navigator['sendBeacon'].apply(navigator, arguments); + }; +} + +var DEFAULT_API_ROUTES = { + 'track': 'track/', + 'engage': 'engage/', + 'groups': 'groups/', + 'record': 'record/' +}; + +/* + * Module-level globals + */ +var DEFAULT_CONFIG = { + 'api_host': 'https://api-js.mixpanel.com', + 'api_routes': DEFAULT_API_ROUTES, + 'api_method': 'POST', + 'api_transport': 'XHR', + 'api_payload_format': PAYLOAD_TYPE_BASE64, + 'app_host': 'https://mixpanel.com', + 'cdn': 'https://cdn.mxpnl.com', + 'cross_site_cookie': false, + 'cross_subdomain_cookie': true, + 'error_reporter': NOOP_FUNC, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_domain': '', + 'cookie_name': '', + 'loaded': NOOP_FUNC, + 'mp_loader': null, + 'track_marketing': true, + 'track_pageview': false, + 'skip_first_touch_marketing': false, + 'store_google': true, + 'stop_utm_persistence': false, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'opt_out_tracking_by_default': false, + 'opt_out_persistence_by_default': false, + 'opt_out_tracking_persistence_type': 'localStorage', + 'opt_out_tracking_cookie_prefix': null, + 'property_blacklist': [], + 'xhr_headers': {}, // { header: value, header2: value } + 'ignore_dnt': false, + 'batch_requests': true, + 'batch_size': 50, + 'batch_flush_interval_ms': 5000, + 'batch_request_timeout_ms': 90000, + 'batch_autostart': true, + 'hooks': {}, + 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), + 'record_block_selector': 'img, video', + 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), + 'record_mask_text_selector': '*', + 'record_max_ms': MAX_RECORDING_MS, + 'record_sessions_percent': 0, + 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' +}; + +var DOM_LOADED = false; + +/** + * Mixpanel Library Object + * @constructor + */ +var MixpanelLib = function() {}; + + +/** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ +var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._cached_groups = {}; // cache groups in a pool + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + if (!instance.get_config('skip_first_touch_marketing')) { + // We need null UTM params in the object because + // UTM parameters act as a tuple. If any UTM param + // is present, then we set all UTM params including + // empty ones together + var utm_params = _.info.campaignParams(null); + var initial_utm_params = {}; + var has_utm = false; + _.each(utm_params, function(utm_value, utm_key) { + initial_utm_params['initial_' + utm_key] = utm_value; + if (utm_value) { + has_utm = true; + } + }); + if (has_utm) { + instance['people'].set_once(initial_utm_params); + } + } + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; +}; + +// Initialization methods + +/** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ +MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + this.report_error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; +}; + +// mixpanel._init(token:string, config:object, name:string) +// +// This function sets up the current instance of the mixpanel +// library. The difference between this method and the init(...) +// method is this one initializes the actual instance, whereas the +// init(...) method sets up a new library and calls _init on it. +// +MixpanelLib.prototype._init = function(token, config, name) { + config = config || {}; + + this['__loaded'] = true; + this['config'] = {}; + + var variable_features = {}; + + // default to JSON payload for standard mixpanel.com API hosts + if (!('api_payload_format' in config)) { + var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; + if (api_host.match(/\.mixpanel\.com/)) { + variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; + } + } + + this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = NOOP_FUNC; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + // set up request queueing/batching + this.request_batchers = {}; + this._batch_requests = this.get_config('batch_requests'); + if (this._batch_requests) { + if (!_.localStorage.is_supported(true) || !USE_XHR) { + this._batch_requests = false; + console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + _.each(this.get_batcher_configs(), function(batcher_config) { + console.log('Clearing batch queue ' + batcher_config.queue_key); + _.localStorage.remove(batcher_config.queue_key); + }); + } else { + this.init_batchers(); + if (sendBeacon && win.addEventListener) { + // Before page closes or hides (user tabs away etc), attempt to flush any events + // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, + // events will not be removed from the persistent store; if the site is loaded again, + // the events will be flushed again on startup and deduplicated on the Mixpanel server + // side. + // There is no reliable way to capture only page close events, so we lean on the + // visibilitychange and pagehide events as recommended at + // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. + // These events fire when the user clicks away from the current page/tab, so will occur + // more frequently than page unload, but are the only mechanism currently for capturing + // this scenario somewhat reliably. + var flush_on_unload = _.bind(function() { + if (!this.request_batchers.events.stopped) { + this.request_batchers.events.flush({unloading: true}); + } + }, this); + win.addEventListener('pagehide', function(ev) { + if (ev['persisted']) { + flush_on_unload(); + } + }); + win.addEventListener('visibilitychange', function() { + if (document$1['visibilityState'] === 'hidden') { + flush_on_unload(); + } + }); + } + } + } + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.unpersisted_superprops = {}; + this._gdpr_init(); + + var uuid = _.UUID(); + if (!this.get_distinct_id()) { + // There is no need to set the distinct id + // or the device id if something was already stored + // in the persitence + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); + } + + var track_pageview_option = this.get_config('track_pageview'); + if (track_pageview_option) { + this._init_url_change_tracking(track_pageview_option); + } + + if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { + this.start_session_recording(); + } +}; + +MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { + if (!win['MutationObserver']) { + console.critical('Browser does not support MutationObserver; skipping session recording'); + return; + } + + var handleLoadedRecorder = _.bind(function() { + this._recorder = this._recorder || new win['__mp_recorder'](this); + this._recorder['startRecording'](); + }, this); + + if (_.isUndefined(win['__mp_recorder'])) { + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); + } else { + handleLoadedRecorder(); + } +}); + +MixpanelLib.prototype.stop_session_recording = function () { + if (this._recorder) { + this._recorder['stopRecording'](); + } else { + console.critical('Session recorder module not loaded'); + } +}; + +MixpanelLib.prototype.get_session_recording_properties = function () { + var props = {}; + if (this._recorder) { + var replay_id = this._recorder['replayId']; + if (replay_id) { + props['$mp_replay_id'] = replay_id; + } + } + return props; +}; + +// Private methods + +MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + this._set_default_superprops(); + this['people'].set_once(this['persistence'].get_referrer_info()); + + // `store_google` is now deprecated and previously stored UTM parameters are cleared + // from persistence by default. + if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { + var utm_params = _.info.campaignParams(null); + _.each(utm_params, function(_utm_value, utm_key) { + // We need to unregister persisted UTM parameters so old values + // are not mixed with the new UTM parameters + this.unregister(utm_key); + }.bind(this)); + } +}; + +// update persistence with info on referrer, UTM params, etc +MixpanelLib.prototype._set_default_superprops = function() { + this['persistence'].update_search_keyword(document$1.referrer); + // Registering super properties for UTM persistence by 'store_google' is deprecated. + if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { + this.register(_.info.campaignParams()); + } + if (this.get_config('save_referrer')) { + this['persistence'].update_referrer_info(document$1.referrer); + } +}; + +MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + + if (!this.has_opted_out_tracking()) { + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + } + + delete this.__dom_loaded_queue; + delete this.__request_queue; +}; + +MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + this.report_error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); +}; + +MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { + var previous_tracked_url = ''; + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = _.info.currentUrl(); + } + + if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { + win.addEventListener('popstate', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + win.addEventListener('hashchange', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + var nativePushState = win.history.pushState; + if (typeof nativePushState === 'function') { + win.history.pushState = function(state, unused, url) { + nativePushState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + var nativeReplaceState = win.history.replaceState; + if (typeof nativeReplaceState === 'function') { + win.history.replaceState = function(state, unused, url) { + nativeReplaceState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + win.addEventListener('mp_locationchange', function() { + var current_url = _.info.currentUrl(); + var should_track = false; + if (track_pageview_option === 'full-url') { + should_track = current_url !== previous_tracked_url; + } else if (track_pageview_option === 'url-with-path-and-query-string') { + should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; + } else if (track_pageview_option === 'url-with-path') { + should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; + } + + if (should_track) { + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = current_url; + } + } + }.bind(this)); + } +}; + +/** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ +MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } +}; + +MixpanelLib.prototype._send_request = function(url, data, options, callback) { + var succeeded = true; + + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return succeeded; + } + + var DEFAULT_OPTIONS = { + method: this.get_config('api_method'), + transport: this.get_config('api_transport'), + verbose: this.get_config('verbose') + }; + var body_data = null; + + if (!callback && (_.isFunction(options) || typeof options === 'string')) { + callback = options; + options = null; + } + options = _.extend(DEFAULT_OPTIONS, options || {}); + if (!USE_XHR) { + options.method = 'GET'; + } + var use_post = options.method === 'POST'; + var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; + + // needed to correctly format responses + var verbose_mode = options.verbose; + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + + if (use_post) { + body_data = 'data=' + encodeURIComponent(data['data']); + delete data['data']; + } + + url += '?' + _.HTTPBuildQuery(data); + + var lib = this; + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (use_sendBeacon) { + try { + succeeded = sendBeacon(url, body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + try { + if (callback) { + callback(succeeded ? 1 : 0); + } + } catch (e) { + lib.report_error(e); + } + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + + var headers = this.get_config('xhr_headers'); + if (use_post) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + + return succeeded; +}; + +/** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ +MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (_.isArray(fn_name)) { + tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() + } else if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + if (_.isArray(item[0])) { + // chained call + var caller = context; + _.each(item, function(call) { + caller = caller[call[0]].apply(caller, call.slice(1)); + }); + } else { + this[item[0]].apply(this, item.slice(1)); + } + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); +}; + +// request queueing utils + +MixpanelLib.prototype.are_batchers_initialized = function() { + return !!this.request_batchers.events; +}; + +MixpanelLib.prototype.get_batcher_configs = function() { + var queue_prefix = '__mpq_' + this.get_config('token'); + var api_routes = this.get_config('api_routes'); + this._batcher_configs = this._batcher_configs || { + events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, + people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, + groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} + }; + return this._batcher_configs; +}; + +MixpanelLib.prototype.init_batchers = function() { + if (!this.are_batchers_initialized()) { + var batcher_for = _.bind(function(attrs) { + return new RequestBatcher( + attrs.queue_key, + { + libConfig: this['config'], + sendRequestFunc: _.bind(function(data, options, cb) { + this._send_request( + this.get_config('api_host') + attrs.endpoint, + this._encode_data_for_request(data), + options, + this._prepare_callback(cb, data) + ); + }, this), + beforeSendHook: _.bind(function(item) { + return this._run_hook('before_send_' + attrs.type, item); + }, this), + errorReporter: this.get_config('error_reporter'), + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + } + ); + }, this); + var batcher_configs = this.get_batcher_configs(); + this.request_batchers = { + events: batcher_for(batcher_configs.events), + people: batcher_for(batcher_configs.people), + groups: batcher_for(batcher_configs.groups) + }; + } + if (this.get_config('batch_autostart')) { + this.start_batch_senders(); + } +}; + +MixpanelLib.prototype.start_batch_senders = function() { + this._batchers_were_started = true; + if (this.are_batchers_initialized()) { + this._batch_requests = true; + _.each(this.request_batchers, function(batcher) { + batcher.start(); + }); + } +}; + +MixpanelLib.prototype.stop_batch_senders = function() { + this._batch_requests = false; + _.each(this.request_batchers, function(batcher) { + batcher.stop(); + batcher.clear(); + }); +}; + +/** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ +MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); +}; + +/** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ +MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } +}; + +MixpanelLib.prototype._encode_data_for_request = function(data) { + var encoded_data = _.JSONEncode(data); + if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { + encoded_data = _.base64Encode(encoded_data); + } + return {'data': encoded_data}; +}; + +// internal method for handling track vs batch-enqueue logic +MixpanelLib.prototype._track_or_batch = function(options, callback) { + var truncated_data = _.truncate(options.data, 255); + var endpoint = options.endpoint; + var batcher = options.batcher; + var should_send_immediately = options.should_send_immediately; + var send_request_options = options.send_request_options || {}; + callback = callback || NOOP_FUNC; + + var request_enqueued_or_initiated = true; + var send_request_immediately = _.bind(function() { + if (!send_request_options.skip_hooks) { + truncated_data = this._run_hook('before_send_' + options.type, truncated_data); + } + if (truncated_data) { + console.log('MIXPANEL REQUEST:'); + console.log(truncated_data); + return this._send_request( + endpoint, + this._encode_data_for_request(truncated_data), + send_request_options, + this._prepare_callback(callback, truncated_data) + ); + } else { + return null; + } + }, this); + + if (this._batch_requests && !should_send_immediately) { + batcher.enqueue(truncated_data, function(succeeded) { + if (succeeded) { + callback(1, truncated_data); + } else { + send_request_immediately(); + } + }); + } else { + request_enqueued_or_initiated = send_request_immediately(); + } + + return request_enqueued_or_initiated && truncated_data; +}; + +/** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // track an event using navigator.sendBeacon + * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this track request. + * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). + * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + var transport = options['transport']; // external API, don't minify 'transport' prop + if (transport) { + options.transport = transport; // 'transport' prop name can be minified internally + } + var should_send_immediately = options['send_immediately']; + if (typeof callback !== 'function') { + callback = NOOP_FUNC; + } + + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = _.extend({}, properties); + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + this._set_default_superprops(); + + var marketing_properties = this.get_config('track_marketing') + ? _.info.marketingParams() + : {}; + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties({'mp_loader': this.get_config('mp_loader')}), + marketing_properties, + this['persistence'].properties(), + this.unpersisted_superprops, + this.get_session_recording_properties(), + properties + ); + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + var ret = this._track_or_batch({ + type: 'events', + data: data, + endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], + batcher: this.request_batchers.events, + should_send_immediately: should_send_immediately, + send_request_options: options + }, callback); + + return ret; +}); + +/** + * Register the current user into one/many groups. + * + * ### Usage: + * + * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs + * mixpanel.set_group('company', 'mixpanel') + * mixpanel.set_group('company', 128746312) + * + * @param {String} group_key Group key + * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * + */ +MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { + if (!_.isArray(group_ids)) { + group_ids = [group_ids]; + } + var prop = {}; + prop[group_key] = group_ids; + this.register(prop); + return this['people'].set(group_key, group_ids, callback); +}); + +/** + * Add a new group for this user. + * + * ### Usage: + * + * mixpanel.add_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_values = this.get_property(group_key); + var prop = {}; + if (old_values === undefined) { + prop[group_key] = [group_id]; + this.register(prop); + } else { + if (old_values.indexOf(group_id) === -1) { + old_values.push(group_id); + prop[group_key] = old_values; + this.register(prop); + } + } + return this['people'].union(group_key, group_id, callback); +}); + +/** + * Remove a group from this user. + * + * ### Usage: + * + * mixpanel.remove_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_value = this.get_property(group_key); + // if the value doesn't exist, the persistent store is unchanged + if (old_value !== undefined) { + var idx = old_value.indexOf(group_id); + if (idx > -1) { + old_value.splice(idx, 1); + this.register({group_key: old_value}); + } + if (old_value.length === 0) { + this.unregister(group_key); + } + } + return this['people'].remove(group_key, group_id, callback); +}); + +/** + * Track an event with specific groups. + * + * ### Usage: + * + * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) + * + * @param {String} event_name The name of the event (see `mixpanel.track()`) + * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) + * @param {Object=} groups An object mapping group name keys to one or more values + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { + var tracking_props = _.extend({}, properties || {}); + _.each(groups, function(v, k) { + if (v !== null && v !== undefined) { + tracking_props[k] = v; + } + }); + return this.track(event_name, tracking_props, callback); +}); + +MixpanelLib.prototype._create_map_key = function (group_key, group_id) { + return group_key + '_' + JSON.stringify(group_id); +}; + +MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { + delete this._cached_groups[this._create_map_key(group_key, group_id)]; +}; + +/** + * Look up reference to a Mixpanel group + * + * ### Usage: + * + * mixpanel.get_group(group_key, group_id) + * + * @param {String} group_key Group key + * @param {Object} group_id A valid Mixpanel property type + * @returns {Object} A MixpanelGroup identifier + */ +MixpanelLib.prototype.get_group = function (group_key, group_id) { + var map_key = this._create_map_key(group_key, group_id); + var group = this._cached_groups[map_key]; + if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { + group = new MixpanelGroup(); + group._init(this, group_key, group_id); + this._cached_groups[map_key] = group; + } + return group; +}; + +/** + * Track a default Mixpanel page view event, which includes extra default event properties to + * improve page view data. + * + * ### Usage: + * + * // track a default $mp_web_page_view event + * mixpanel.track_pageview(); + * + * // track a page view event with additional event properties + * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); + * + * // example approach to track page views on different page types as event properties + * mixpanel.track_pageview({'page': 'pricing'}); + * mixpanel.track_pageview({'page': 'homepage'}); + * + * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for + * // individual pages on the same site or product. Use cases for custom event_name may be page + * // views on different products or internal applications that are considered completely separate + * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); + * + * ### Notes: + * + * The `config.track_pageview` option for mixpanel.init() + * may be turned on for tracking page loads automatically. + * + * // track only page loads + * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); + * + * // track when the URL changes in any manner + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); + * + * // track when the URL changes, ignoring any changes in the hash part + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); + * + * // track when the path changes, ignoring any query parameter or hash changes + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); + * + * @param {Object} [properties] An optional set of additional properties to send with the page view event + * @param {Object} [options] Page view tracking options + * @param {String} [options.event_name] - Alternate name for the tracking event + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { + if (typeof properties !== 'object') { + properties = {}; + } + options = options || {}; + var event_name = options['event_name'] || '$mp_web_page_view'; + + var default_page_properties = _.extend( + _.info.mpPageViewProperties(), + _.info.campaignParams(), + _.info.clickParams() + ); + + var event_properties = _.extend( + {}, + default_page_properties, + properties + ); + + return this.track(event_name, event_properties); +}); + +/** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ +MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); +}; + +/** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ +MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); +}; + +/** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ +MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); +}; + +var REGISTER_DEFAULTS = { + 'persistent': true +}; +/** + * Helper to parse options param for register methods, maintaining + * legacy support for plain "days" param instead of options object + * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods + * @returns {Object} options object + */ +var options_for_register = function(days_or_options) { + var options; + if (_.isObject(days_or_options)) { + options = days_or_options; + } else if (!_.isUndefined(days_or_options)) { + options = {'days': days_or_options}; + } else { + options = {}; + } + return _.extend({}, REGISTER_DEFAULTS, options); +}; + +/** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * // register only for the current pageload + * mixpanel.register({'Name': 'Pat'}, {persistent: false}); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register = function(props, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register(props, options['days']); + } else { + _.extend(this.unpersisted_superprops, props); + } +}; + +/** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * // register once, only for the current pageload + * mixpanel.register_once({ + * 'First interaction time': new Date().toISOString() + * }, 'None', {persistent: false}); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register_once(props, default_value, options['days']); + } else { + if (typeof(default_value) === 'undefined') { + default_value = 'None'; + } + _.each(props, function(val, prop) { + if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { + this.unpersisted_superprops[prop] = val; + } + }, this); + } +}; + +/** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + * @param {Object} [options] + * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.unregister = function(property, options) { + options = options_for_register(options); + if (options['persistent']) { + this['persistence'].unregister(property); + } else { + delete this.unpersisted_superprops[property]; + } +}; + +MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); +}; + +/** + * Identify a user with a unique ID to track user activity across + * devices, tie a user to their events, and create a user profile. + * If you never call this method, unique visitors are tracked using + * a UUID generated the first time they visit the site. + * + * Call identify when you know the identity of the current user, + * typically after login or signup. We recommend against using + * identify for anonymous visitors to your site. + * + * ### Notes: + * If your project has + * ID Merge + * enabled, the identify method will connect pre- and + * post-authentication events when appropriate. + * + * If your project does not have ID Merge enabled, identify will + * change the user's local distinct_id to the unique ID you pass. + * Events tracked prior to authentication will not be connected + * to the same user identity. If ID Merge is disabled, alias can + * be used to connect pre- and post-registration events. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ +MixpanelLib.prototype.identify = function( + new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + var previous_distinct_id = this.get_distinct_id(); + if (new_distinct_id && previous_distinct_id !== new_distinct_id) { + // we allow the following condition if previous distinct_id is same as new_distinct_id + // so that you can force flush people updates for anonymous profiles. + if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { + this.report_error('distinct_id cannot have $device: prefix'); + return -1; + } + this.register({'$user_id': new_distinct_id}); + } + + if (!this.get_property('$device_id')) { + // The persisted distinct id might not actually be a device id at all + // it might be a distinct id of the user from before + var device_id = previous_distinct_id; + this.register_once({ + '$had_persisted_distinct_id': true, + '$device_id': device_id + }, ''); + } + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this.register({'distinct_id': new_distinct_id}); + } + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); + + // send an $identify event any time the distinct_id is changing - logic on the server + // will determine whether or not to do anything with it. + if (new_distinct_id !== previous_distinct_id) { + this.track('$identify', { + 'distinct_id': new_distinct_id, + '$anon_distinct_id': previous_distinct_id + }, {skip_hooks: true}); + } +}; + +/** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ +MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + var uuid = _.UUID(); + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); +}; + +/** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ +MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); +}; + +/** + * The alias method creates an alias which Mixpanel will use to + * remap one id to another. Multiple aliases can point to the + * same identifier. + * + * The following is a valid use of alias: + * + * mixpanel.alias('new_id', 'existing_id'); + * // You can add multiple id aliases to the existing ID + * mixpanel.alias('newer_id', 'existing_id'); + * + * Aliases can also be chained - the following is a valid example: + * + * mixpanel.alias('new_id', 'existing_id'); + * // chain newer_id - new_id - existing_id + * mixpanel.alias('newer_id', 'new_id'); + * + * Aliases cannot point to multiple identifiers - the following + * example will not work: + * + * mixpanel.alias('new_id', 'existing_id'); + * // this is invalid as 'new_id' already points to 'existing_id' + * mixpanel.alias('new_id', 'newer_id'); + * + * ### Notes: + * + * If your project does not have + * ID Merge + * enabled, the best practice is to call alias once when a unique + * ID is first created for a user (e.g., when a user first registers + * for an account). Do not use alias multiple times for a single + * user without ID Merge enabled. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ +MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + this.report_error('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { + 'alias': alias, + 'distinct_id': original + }, { + skip_hooks: true + }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + this.report_error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } +}; + +/** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @deprecated + */ +MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); +}; + +/** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // host for requests (customizable for e.g. a local proxy) + * api_host: 'https://api-js.mixpanel.com', + * + * // endpoints for different types of requests + * api_routes: { + * track: 'track/', + * engage: 'engage/', + * groups: 'groups/', + * } + * + * // HTTP method for tracking requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. Mixpanel + * // tracking via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // request-batching/queueing/retry + * batch_requests: true, + * + * // maximum number of events/updates to send in a single + * // network request + * batch_size: 50, + * + * // milliseconds to wait between sending batch requests + * batch_flush_interval_ms: 5000, + * + * // milliseconds to wait for network responses to batch requests + * // before they are considered timed-out and retried + * batch_request_timeout_ms: 90000, + * + * // override value for cookie domain, only useful for ensuring + * // correct cross-subdomain cookies on unusual domains like + * // subdomain.mainsite.avocat.fr; NB this cannot be used to + * // set cookies on a different domain than the current origin + * cookie_domain: '' + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // if true, cookie will be set with SameSite=None; Secure + * // this is only useful in special situations, like embedded + * // 3rd-party iframes that set up a Mixpanel instance + * cross_site_cookie: false + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of tracking by this Mixpanel instance by default + * opt_out_tracking_by_default: false + * + * // opt users out of browser data storage by this Mixpanel instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_tracking_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_tracking_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // disables enriching user profiles with first touch marketing data + * skip_first_touch_marketing: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // adds any UTM parameters and click IDs present on the page to any events fired + * track_marketing: true + * + * // enables automatic page view tracking using default page view events through + * // the track_pageview() method + * track_pageview: false + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // whether to ignore or respect the web browser's Do Not Track setting + * ignore_dnt: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ +MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + var new_batch_size = config['batch_size']; + if (new_batch_size) { + _.each(this.request_batchers, function(batcher) { + batcher.resetBatchSize(); + }); + } + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } +}; + +/** + * returns the current config object for the library. + */ +MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; +}; + +/** + * Fetch a hook function from config, with safe default, and run it + * against the given arguments + * @param {string} hook_name which hook to retrieve + * @returns {any|null} return value of user-provided hook, or null if nothing was returned + */ +MixpanelLib.prototype._run_hook = function(hook_name) { + var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); + if (typeof ret === 'undefined') { + this.report_error(hook_name + ' hook did not return a value'); + ret = null; + } + return ret; +}; + +/** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ +MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence'].load_prop([property_name]); +}; + +MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; +}; + +MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); +}; + +// perform some housekeeping around GDPR opt-in/out state +MixpanelLib.prototype._gdpr_init = function() { + var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; + + // try to convert opt-in/out cookies to localStorage if possible + if (is_localStorage_requested && _.localStorage.is_supported()) { + if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { + this.opt_in_tracking({'enable_persistence': false}); + } + if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { + this.opt_out_tracking({'clear_persistence': false}); + } + this.clear_opt_in_out_tracking({ + 'persistence_type': 'cookie', + 'enable_persistence': false + }); + } + + // check whether the user has already opted out - if so, clear & disable persistence + if (this.has_opted_out_tracking()) { + this._gdpr_update_persistence({'clear_persistence': true}); + + // check whether we should opt out by default + // note: we don't clear persistence here by default since opt-out default state is often + // used as an initial state while GDPR information is being collected + } else if (!this.has_opted_in_tracking() && ( + this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') + )) { + _.cookie.remove('mp_optout'); + this.opt_out_tracking({ + 'clear_persistence': this.get_config('opt_out_persistence_by_default') + }); + } +}; + +/** + * Enable or disable persistence based on options + * only enable/disable if persistence is not already in this state + * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it + * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence + */ +MixpanelLib.prototype._gdpr_update_persistence = function(options) { + var disabled; + if (options && options['clear_persistence']) { + disabled = true; + } else if (options && options['enable_persistence']) { + disabled = false; + } else { + return; + } + + if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { + this['persistence'].set_disabled(disabled); + } + + if (disabled) { + this.stop_batch_senders(); + } else { + // only start batchers after opt-in if they have previously been started + // in order to avoid unintentionally starting up batching for the first time + if (this._batchers_were_started) { + this.start_batch_senders(); + } + } +}; + +// call a base gdpr function after constructing the appropriate token and options args +MixpanelLib.prototype._gdpr_call_func = function(func, options) { + options = _.extend({ + 'track': _.bind(this.track, this), + 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), + 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), + 'cookie_expiration': this.get_config('cookie_expiration'), + 'cross_site_cookie': this.get_config('cross_site_cookie'), + 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), + 'cookie_domain': this.get_config('cookie_domain'), + 'secure_cookie': this.get_config('secure_cookie'), + 'ignore_dnt': this.get_config('ignore_dnt') + }, options); + + // check if localStorage can be used for recording opt out status, fall back to cookie if not + if (!_.localStorage.is_supported()) { + options['persistence_type'] = 'cookie'; + } + + return func(this.get_config('token'), { + track: options['track'], + trackEventName: options['track_event_name'], + trackProperties: options['track_properties'], + persistenceType: options['persistence_type'], + persistencePrefix: options['cookie_prefix'], + cookieDomain: options['cookie_domain'], + cookieExpiration: options['cookie_expiration'], + crossSiteCookie: options['cross_site_cookie'], + crossSubdomainCookie: options['cross_subdomain_cookie'], + secureCookie: options['secure_cookie'], + ignoreDnt: options['ignore_dnt'] + }); +}; + +/** + * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user in + * mixpanel.opt_in_tracking(); + * + * // opt user in with specific event name, properties, cookie configuration + * mixpanel.opt_in_tracking({ + * track_event_name: 'User opted in', + * track_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) + * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action + * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_in_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(optIn, options); + this._gdpr_update_persistence(options); +}; + +/** + * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user out + * mixpanel.opt_out_tracking(); + * + * // opt user out with different cookie configuration from Mixpanel instance + * mixpanel.opt_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_out_tracking = function(options) { + options = _.extend({ + 'clear_persistence': true, + 'delete_user': true + }, options); + + // delete user and clear charges since these methods may be disabled by opt-out + if (options['delete_user'] && this['people'] && this['people']._identify_called()) { + this['people'].delete_user(); + this['people'].clear_charges(); + } + + this._gdpr_call_func(optOut, options); + this._gdpr_update_persistence(options); +}; + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_in = mixpanel.has_opted_in_tracking(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ +MixpanelLib.prototype.has_opted_in_tracking = function(options) { + return this._gdpr_call_func(hasOptedIn, options); +}; + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_out = mixpanel.has_opted_out_tracking(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ +MixpanelLib.prototype.has_opted_out_tracking = function(options) { + return this._gdpr_call_func(hasOptedOut, options); +}; + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // clear user's opt-in/out status + * mixpanel.clear_opt_in_out_tracking(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_tracking/opt_out_tracking methods were called. + * mixpanel.clear_opt_in_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(clearOptInOut, options); + this._gdpr_update_persistence(options); +}; + +MixpanelLib.prototype.report_error = function(msg, err) { + console.error.apply(console.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + console.error(err); + } +}; + +// EXPORTS (for closure compiler) + +// MixpanelLib Exports +MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; +MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; +MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; +MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; +MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; +MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; +MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; +MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; +MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; +MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; +MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; +MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; +MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; +MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; +MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; +MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; +MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; +MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; +MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; +MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; +MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; +MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; +MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; +MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; +MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; +MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; +MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; +MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; +MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; +MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; +MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; +MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; +MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; +MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; +MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; + +// MixpanelPersistence Exports +MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; +MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; +MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; +MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; +MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + +var instances = {}; +var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; +}; + +var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; +}; + +var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); +}; + +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; + init_type = INIT_MODULE; + mixpanel_master = new MixpanelLib(); + + override_mp_init_func(); + mixpanel_master['init'](); + add_dom_loaded_handler(); + + return mixpanel_master; +} + +// For loading separate bundles asynchronously via script tag +// so that we don't load them until they are needed at runtime. +function loadAsync (src, onload) { + var scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + scriptEl.async = true; + scriptEl.onload = onload; + scriptEl.src = src; + document.head.appendChild(scriptEl); +} + +/* eslint camelcase: "off" */ + +var mixpanel = init_as_module(loadAsync); + +module.exports = mixpanel; diff --git a/dist/mixpanel-main.cjs.js b/dist/mixpanel-main.cjs.js new file mode 100644 index 00000000..128f89bd --- /dev/null +++ b/dist/mixpanel-main.cjs.js @@ -0,0 +1,6330 @@ +'use strict'; + +var Config = { + DEBUG: false, + LIB_VERSION: '2.53.0' +}; + +/* eslint camelcase: "off", eqeqeq: "off" */ + +// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file +var win; +if (typeof(window) === 'undefined') { + var loc = { + hostname: '' + }; + win = { + navigator: { userAgent: '' }, + document: { + location: loc, + referrer: '' + }, + screen: { width: 0, height: 0 }, + location: loc + }; +} else { + win = window; +} + +// Maximum allowed session recording length +var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours + +/* + * Saved references to long variable names, so that closure compiler can + * minimize file size. + */ + +var ArrayProto = Array.prototype, + FuncProto = Function.prototype, + ObjProto = Object.prototype, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty, + windowConsole = win.console, + navigator = win.navigator, + document$1 = win.document, + windowOpera = win.opera, + screen = win.screen, + userAgent = navigator.userAgent; + +var nativeBind = FuncProto.bind, + nativeForEach = ArrayProto.forEach, + nativeIndexOf = ArrayProto.indexOf, + nativeMap = ArrayProto.map, + nativeIsArray = Array.isArray, + breaker = {}; + +var _ = { + trim: function(str) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } +}; + +// Console override +var console = { + /** @type {function(...*)} */ + log: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } + }, + /** @type {function(...*)} */ + warn: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); + try { + windowConsole.warn.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.warn(arg); + }); + } + } + }, + /** @type {function(...*)} */ + error: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + }, + /** @type {function(...*)} */ + critical: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + } +}; + +var log_func_with_prefix = function(func, prefix) { + return function() { + arguments[0] = '[' + prefix + '] ' + arguments[0]; + return func.apply(console, arguments); + }; +}; +var console_with_prefix = function(prefix) { + return { + log: log_func_with_prefix(console.log, prefix), + error: log_func_with_prefix(console.error, prefix), + critical: log_func_with_prefix(console.critical, prefix) + }; +}; + + +// UNDERSCORE +// Embed part of the Underscore Library +_.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + if (!_.isFunction(func)) { + throw new TypeError(); + } + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) { + return func.apply(context, args.concat(slice.call(arguments))); + } + var ctor = {}; + ctor.prototype = func.prototype; + var self = new ctor(); + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; +}; + +/** + * @param {*=} obj + * @param {function(...*)=} iterator + * @param {Object=} context + */ +_.each = function(obj, iterator, context) { + if (obj === null || obj === undefined) { + return; + } + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { + return; + } + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) { + return; + } + } + } + } +}; + +_.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; +}; + +_.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; +}; + +// from a comment on http://dbj.org/dbj/?p=286 +// fails on only one very rare and deliberate custom object: +// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; +_.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } +}; + +_.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); +}; + +_.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); +}; + +_.map = function(arr, callback, context) { + if (nativeMap && arr.map === nativeMap) { + return arr.map(callback, context); + } else { + var results = []; + _.each(arr, function(item) { + results.push(callback.call(context, item)); + }); + return results; + } +}; + +_.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; +}; + +_.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; +}; + +_.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; +}; + +_.includes = function(str, needle) { + return str.indexOf(needle) !== -1; +}; + +// Underscore Addons +_.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; +}; + +_.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); +}; + +_.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; +}; + +_.isUndefined = function(obj) { + return obj === void 0; +}; + +_.isString = function(obj) { + return toString.call(obj) == '[object String]'; +}; + +_.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; + +_.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; +}; + +_.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); +}; + +_.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; +}; + +_.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); +}; + +_.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); +}; + +_.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; +}; + +/* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ +_.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; +}; + +_.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; +})(); + +/** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ +_.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; +})(); + +_.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; +}; + +_.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; +}; + +_.UUID = (function() { + + // Time-based entropy + var T = function() { + var time = 1 * new Date(); // cross-browser version of Date.now() + var ticks; + if (win.performance && win.performance.now) { + ticks = win.performance.now(); + } else { + // fall back to busy loop + ticks = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (time == 1 * new Date()) { + ticks++; + } + } + return time.toString(16) + Math.floor(ticks).toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; +})(); + +// _.isBlockedUA() +// This is to block various web spiders from executing our JS and +// sending false tracking data +var BLOCKED_UA_STRS = [ + 'ahrefsbot', + 'ahrefssiteaudit', + 'baiduspider', + 'bingbot', + 'bingpreview', + 'chrome-lighthouse', + 'facebookexternal', + 'petalbot', + 'pinterest', + 'screaming frog', + 'yahoo! slurp', + 'yandexbot', + + // a whole bunch of goog-specific crawlers + // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers + 'adsbot-google', + 'apis-google', + 'duplexweb-google', + 'feedfetcher-google', + 'google favicon', + 'google web preview', + 'google-read-aloud', + 'googlebot', + 'googleweblight', + 'mediapartners-google', + 'storebot-google' +]; +_.isBlockedUA = function(ua) { + var i; + ua = ua.toLowerCase(); + for (i = 0; i < BLOCKED_UA_STRS.length; i++) { + if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { + return true; + } + } + return false; +}; + +/** + * @param {Object=} formdata + * @param {string=} arg_separator + */ +_.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); +}; + +_.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + var result = results[1]; + try { + result = decodeURIComponent(result); + } catch(err) { + console.error('Skipping decoding for malformed query param: ' + result); + } + return result.replace(/\+/g, ' '); + } +}; + + +// _.cookie +// Methods partially borrowed from quirksmode.org/js/cookies.html +_.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', + expires = '', + secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', expires = '', secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, is_cross_subdomain, domain_override) { + _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); + } +}; + +var _localStorageSupported = null; +var localStorageSupported = function(storage, forceCheck) { + if (_localStorageSupported !== null && !forceCheck) { + return _localStorageSupported; + } + + var supported = true; + try { + storage = storage || window.localStorage; + var key = '__mplss_' + cheap_guid(8), + val = 'xyz'; + storage.setItem(key, val); + if (storage.getItem(key) !== val) { + supported = false; + } + storage.removeItem(key); + } catch (err) { + supported = false; + } + + _localStorageSupported = supported; + return supported; +}; + +// _.localStorage +_.localStorage = { + is_supported: function(force_check) { + var supported = localStorageSupported(null, force_check); + if (!supported) { + console.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }, + + error: function(msg) { + console.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } +}; + +_.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...*)} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; +})(); + + +var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); + +_.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(TOKEN_MATCH_REGEX); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; +})(); + +var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; +var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; + +_.info = { + campaignParams: function(default_value) { + var kw = '', + params = {}; + _.each(CAMPAIGN_KEYWORDS, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } else if (default_value !== undefined) { + params[kwkey] = default_value; + } + }); + + return params; + }, + + clickParams: function() { + var id = '', + params = {}; + _.each(CLICK_IDS, function(idkey) { + id = _.getQueryParam(document$1.URL, idkey); + if (id.length) { + params[idkey] = id; + } + }); + + return params; + }, + + marketingParams: function() { + return _.extend(_.info.campaignParams(), _.info.clickParams()); + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'SamsungBrowser/')) { + // https://developer.samsung.com/internet/user-agent-string-format + return 'Samsung Internet'; + } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + currentUrl: function() { + return win.location.href; + }, + + properties: function(extra_props) { + if (typeof extra_props !== 'object') { + extra_props = {}; + } + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': _.info.currentUrl(), + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION, + '$insert_id': cheap_guid(), + 'time': _.timestamp() / 1000 // epoch time in seconds + }, _.strip_empty_properties(extra_props)); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) + }); + }, + + mpPageViewProperties: function() { + return _.strip_empty_properties({ + 'current_page_title': document$1.title, + 'current_domain': win.location.hostname, + 'current_url_path': win.location.pathname, + 'current_url_protocol': win.location.protocol, + 'current_url_search': win.location.search + }); + } +}; + +var cheap_guid = function(maxlen) { + var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); + return maxlen ? guid.substring(0, maxlen) : guid; +}; + +// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) +var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; +// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk +var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; +/** + * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For + * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we + * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). + * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy + * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel + * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date + * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK + * offers the 'cookie_domain' config option to set it explicitly. + * @example + * extract_domain('my.sub.example.com') + * // 'example.com' + */ +var extract_domain = function(hostname) { + var domain_regex = DOMAIN_MATCH_REGEX; + var parts = hostname.split('.'); + var tld = parts[parts.length - 1]; + if (tld.length > 4 || tld === 'com' || tld === 'org') { + domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; + } + var matches = hostname.match(domain_regex); + return matches ? matches[0] : ''; +}; + +var JSONStringify = null, JSONParse = null; +if (typeof JSON !== 'undefined') { + JSONStringify = JSON.stringify; + JSONParse = JSON.parse; +} +JSONStringify = JSONStringify || _.JSONEncode; +JSONParse = JSONParse || _.JSONDecode; + +// EXPORTS (for closure compiler) +_['toArray'] = _.toArray; +_['isObject'] = _.isObject; +_['JSONEncode'] = _.JSONEncode; +_['JSONDecode'] = _.JSONDecode; +_['isBlockedUA'] = _.isBlockedUA; +_['isEmptyObject'] = _.isEmptyObject; +_['info'] = _.info; +_['info']['device'] = _.info.device; +_['info']['browser'] = _.info.browser; +_['info']['browserVersion'] = _.info.browserVersion; +_['info']['properties'] = _.info.properties; + +/* eslint camelcase: "off" */ + +/** + * DomTracker Object + * @constructor + */ +var DomTracker = function() {}; + + +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; + +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; + +/** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback + */ +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; +}; + +/** + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; +}; + +DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; + +var logger$2 = console_with_prefix('lock'); + +/** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ +var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; +}; + +// pass in a specific pid to test contention scenarios; otherwise +// it is chosen randomly for each acquisition attempt +SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } +}; + +var logger$1 = console_with_prefix('batch'); + +/** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ +var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; +}; + +/** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ +RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ +RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; +}; + +/** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ +var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; +}; + +/** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ +RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); +}; + +// internal helper for RequestQueue.updatePayloads +var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; +}; + +/** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ +RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ +RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; +}; + +/** + * Serialize the given items array to localStorage. + */ +RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } +}; + +/** + * Clear out queues (memory and localStorage). + */ +RequestQueue.prototype.clear = function() { + this.memQueue = []; + this.storage.removeItem(this.storageKey); +}; + +// maximum interval between request retries after exponential backoff +var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + +var logger = console_with_prefix('batch'); + +/** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ +var RequestBatcher = function(storageKey, options) { + this.errorReporter = options.errorReporter; + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage + }); + + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; + + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; + + this.stopped = !this.libConfig['batch_autostart']; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; +}; + +/** + * Add one item to queue. + */ +RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.flushInterval, cb); +}; + +/** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ +RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); +}; + +/** + * Stop flushing batches. Can be restarted by calling start(). + */ +RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } +}; + +/** + * Clear out queue. + */ +RequestBatcher.prototype.clear = function() { + this.queue.clear(); +}; + +/** + * Restore batch size configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetBatchSize = function() { + this.batchSize = this.libConfig['batch_size']; +}; + +/** + * Restore flush interval time configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); +}; + +/** + * Schedule the next flush in the given number of milliseconds. + */ +RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.flushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + } +}; + +/** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ +RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var startTime = new Date().getTime(); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + res.xhr_req && + (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + var headers = res.xhr_req['responseHeaders']; + if (headers) { + var retryAfter = headers['Retry-After']; + if (retryAfter) { + retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; + } + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + this.flush(); // handle next batch if the queue isn't empty + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.stopAllBatching(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger.log('MIXPANEL REQUEST:', dataForRequest); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } +}; + +/** + * Log error to global logger and optional user-defined logger. + */ +RequestBatcher.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + if (this.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.errorReporter(msg, err); + } catch(err) { + logger.error(err); + } + } +}; + +/** + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. + */ + +/** + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + +/** Public **/ + +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} + +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type + */ +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} + +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} + +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} + +/** Private **/ + +/** + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage + */ +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} + +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} + +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} + +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; + + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); + + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; + } + + options = options || {}; + + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} + +/** + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; + + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } + + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +/* eslint camelcase: "off" */ + +/** @const */ var SET_ACTION = '$set'; +/** @const */ var SET_ONCE_ACTION = '$set_once'; +/** @const */ var UNSET_ACTION = '$unset'; +/** @const */ var ADD_ACTION = '$add'; +/** @const */ var APPEND_ACTION = '$append'; +/** @const */ var UNION_ACTION = '$union'; +/** @const */ var REMOVE_ACTION = '$remove'; +/** @const */ var DELETE_ACTION = '$delete'; + +// Common internal methods for mixpanel.people and mixpanel.group APIs. +// These methods shouldn't involve network I/O. +var apiActions = { + set_action: function(prop, to) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + } else { + $set[prop] = to; + } + + data[SET_ACTION] = $set; + return data; + }, + + unset_action: function(prop) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + return data; + }, + + set_once_action: function(prop, to) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return data; + }, + + union_action: function(list_name, values) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + return data; + }, + + append_action: function(list_name, value) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + return data; + }, + + remove_action: function(list_name, value) { + var data = {}; + var $remove = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $remove[k] = v; + } + }, this); + } else { + $remove[list_name] = value; + } + data[REMOVE_ACTION] = $remove; + return data; + }, + + delete_action: function() { + var data = {}; + data[DELETE_ACTION] = ''; + return data; + } +}; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel Group Object + * @constructor + */ +var MixpanelGroup = function() {}; + +_.extend(MixpanelGroup.prototype, apiActions); + +MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { + this._mixpanel = mixpanel_instance; + this._group_key = group_key; + this._group_id = group_id; +}; + +/** + * Set properties on a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Set properties on a group, only if they do not yet exist. + * This will not overwrite previous group property values, unlike + * group.set(). + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set_once({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, lists or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Unset properties on a group permanently. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').unset('Founded'); + * + * @param {String} prop The name of the property. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/** + * Merge a given list with a list-valued group property, excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); + * + * @param {String} list_name Name of the property. + * @param {Array} values Values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/** + * Permanently delete a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').delete(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { + // bracket notation above prevents a minification error related to reserved words + var data = this.delete_action(); + return this._send_request(data, callback); +}); + +/** + * Remove a property from a group. The value will be ignored if doesn't exist. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); + * + * @param {String} list_name Name of the property. + * @param {Object} value Value to remove from the given group property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +MixpanelGroup.prototype._send_request = function(data, callback) { + data['$group_key'] = this._group_key; + data['$group_id'] = this._group_id; + data['$token'] = this._get_config('token'); + + var date_encoded_data = _.encodeDates(data); + return this._mixpanel._track_or_batch({ + type: 'groups', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], + batcher: this._mixpanel.request_batchers.groups + }, callback); +}; + +MixpanelGroup.prototype._is_reserved_property = function(prop) { + return prop === '$group_key' || prop === '$group_id'; +}; + +MixpanelGroup.prototype._get_config = function(conf) { + return this._mixpanel.get_config(conf); +}; + +MixpanelGroup.prototype.toString = function() { + return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; +}; + +// MixpanelGroup Exports +MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; +MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; +MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; +MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; +MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; +MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel People Object + * @constructor + */ +var MixpanelPeople = function() {}; + +_.extend(MixpanelPeople.prototype, apiActions); + +MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; +}; + +/* +* Set properties on a user record. +* +* ### Usage: +* +* mixpanel.people.set('gender', 'm'); +* +* // or set multiple properties at once +* mixpanel.people.set({ +* 'Company': 'Acme', +* 'Plan': 'Premium', +* 'Upgrade date': new Date() +* }); +* // properties can be strings, integers, dates, or lists +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document.referrer); + } + + // update $set object with default people properties + data[SET_ACTION] = _.extend( + {}, + _.info.people_properties(), + data[SET_ACTION] + ); + return this._send_request(data, callback); +}); + +/* +* Set properties on a user record, only if they do not yet exist. +* This will not overwrite previous people property values, unlike +* people.set(). +* +* ### Usage: +* +* mixpanel.people.set_once('First Login Date', new Date()); +* +* // or set multiple properties at once +* mixpanel.people.set_once({ +* 'First Login Date': new Date(), +* 'Starting Plan': 'Premium' +* }); +* +* // properties can be strings, integers or dates +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/* +* Unset properties on a user record (permanently removes the properties and their values from a profile). +* +* ### Usage: +* +* mixpanel.people.unset('gender'); +* +* // or unset multiple properties at once +* mixpanel.people.unset(['gender', 'Company']); +* +* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/* +* Increment/decrement numeric people analytics properties. +* +* ### Usage: +* +* mixpanel.people.increment('page_views', 1); +* +* // or, for convenience, if you're just incrementing a counter by +* // 1, you can simply do +* mixpanel.people.increment('page_views'); +* +* // to decrement a counter, pass a negative number +* mixpanel.people.increment('credits_left', -1); +* +* // like mixpanel.people.set(), you can increment multiple +* // properties at once: +* mixpanel.people.increment({ +* counter1: 1, +* counter2: 6 +* }); +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. +* @param {Number} [by] An amount to increment the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); +}); + +/* +* Append a value to a list-valued people analytics property. +* +* ### Usage: +* +* // append a value to a list, creating it if needed +* mixpanel.people.append('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.append({ +* list1: 'bob', +* list2: 123 +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value An item to append to the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.append_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Remove a value from a list-valued people analytics property. +* +* ### Usage: +* +* mixpanel.people.remove('School', 'UCB'); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value Item to remove from the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Merge a given list with a list-valued people analytics property, +* excluding duplicate values. +* +* ### Usage: +* +* // merge a value to a list, creating it if needed +* mixpanel.people.union('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.union({ +* list1: 'bob', +* list2: 123 +* }); +* +* // like mixpanel.people.append(), you can append multiple +* // values to the same list: +* mixpanel.people.union({ +* list1: ['bob', 'billy'] +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] Value / values to merge with the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + * @deprecated + */ +MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); +}); + +/* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * @deprecated + */ +MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); +}; + +/* +* Permanently deletes the current people analytics profile from +* Mixpanel (using the current distinct_id). +* +* ### Usage: +* +* // remove the all data you have stored about the current user +* mixpanel.people.delete_user(); +* +*/ +MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); +}; + +MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; +}; + +MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + var device_id = this._mixpanel.get_property('$device_id'); + var user_id = this._mixpanel.get_property('$user_id'); + var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); + if (device_id) { + data['$device_id'] = device_id; + } + if (user_id) { + data['$user_id'] = user_id; + } + if (had_persisted_distinct_id) { + data['$had_persisted_distinct_id'] = had_persisted_distinct_id; + } + + var date_encoded_data = _.encodeDates(data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return _.truncate(date_encoded_data, 255); + } + + return this._mixpanel._track_or_batch({ + type: 'people', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], + batcher: this._mixpanel.request_batchers.people + }, callback); +}; + +MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); +}; + +MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; +}; + +// Queue up engage operations if identify hasn't been called yet. +MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (REMOVE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console.error('Invalid call to _enqueue():', data); + } +}; + +MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + _this._mixpanel['persistence'].save(); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } +}; + +// Flush queued engage operations - order does not matter, +// and there are network level race conditions anyway +MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + var _this = this; + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var append_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + $append_item = $append_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($append_item)) { + _this.append($append_item, append_callback); + } + } + } + + // same for $remove + var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { + var $remove_item; + var remove_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); + } + if (!_.isUndefined(_remove_callback)) { + _remove_callback(response, data); + } + }; + for (var j = $remove_queue.length - 1; j >= 0; j--) { + $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + $remove_item = $remove_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($remove_item)) { + _this.remove($remove_item, remove_callback); + } + } + } +}; + +MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; +}; + +// MixpanelPeople Exports +MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; +MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; +MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; +MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; +MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; +MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; +MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; +MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; +MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; +MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; +MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + +/* eslint camelcase: "off" */ + +/* + * Constants + */ +/** @const */ var SET_QUEUE_KEY = '__mps'; +/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; +/** @const */ var UNSET_QUEUE_KEY = '__mpus'; +/** @const */ var ADD_QUEUE_KEY = '__mpa'; +/** @const */ var APPEND_QUEUE_KEY = '__mpap'; +/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; +/** @const */ var UNION_QUEUE_KEY = '__mpu'; +// This key is deprecated, but we want to check for it to see whether aliasing is allowed. +/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; +/** @const */ var ALIAS_ID_KEY = '__alias'; +/** @const */ var EVENT_TIMERS_KEY = '__timers'; +/** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + REMOVE_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + EVENT_TIMERS_KEY +]; + +/** + * Mixpanel Persistence Object + * @constructor + */ +var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + if (storage_type === 'localStorage' && _.localStorage.is_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(); + this.save(); +}; + +MixpanelPersistence.prototype.properties = function() { + var p = {}; + + this.load(); + + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; +}; + +MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } +}; + +MixpanelPersistence.prototype.upgrade = function() { + var old_cookie, + old_localstorage; + + // if transferring from cookie to localStorage or vice-versa, copy existing + // super properties over to new storage mode + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } else if (this.storage === _.cookie) { + old_localstorage = _.localStorage.parse(this.name); + + _.localStorage.remove(this.name); + + if (old_localstorage) { + this.register_once(old_localstorage); + } + } +}; + +MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure, + this.cross_site, + this.cookie_domain + ); +}; + +MixpanelPersistence.prototype.load_prop = function(key) { + this.load(); + return this['props'][key]; +}; + +MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false, this.cookie_domain); + this.storage.remove(this.name, true, this.cookie_domain); +}; + +// removes the storage entry and deletes all loaded data +// forced name for tests +MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; +}; + +/** +* @param {Object} props +* @param {*=} default_value +* @param {number=} days +*/ +MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; +}; + +/** +* @param {Object} props +* @param {number=} days +*/ +MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + _.extend(this['props'], props); + this.save(); + + return true; + } + return false; +}; + +MixpanelPersistence.prototype.unregister = function(prop) { + this.load(); + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } +}; + +MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); +}; + +// EXPORTED METHOD, we test this directly. +MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); +}; + +MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); +}; + +MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cookie_domain(config['cookie_domain']); + this.set_cross_site(config['cross_site_cookie']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); +}; + +MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } else { + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { + if (cookie_domain !== this.cookie_domain) { + this.remove(); + this.cookie_domain = cookie_domain; + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_site = function(cross_site) { + if (cross_site !== this.cross_site) { + this.cross_site = cross_site; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; +}; + +MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + remove_q = this._get_or_create_queue(REMOVE_ACTION, []), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === REMOVE_QUEUE_KEY) { + remove_q.push(q_data); + this._pop_from_people_queue(APPEND_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console.log(data); + + this.save(); +}; + +MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this['props'][this._get_queue_key(queue)]; + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { + // list actions: only remove if both k+v match + // e.g. remove should not override append in a case like + // append({foo: 'bar'}); remove({foo: 'qux'}) + _.each(q, function(queued_action) { + if (queued_action[k] === v) { + delete queued_action[k]; + } + }); + } else { + delete q[k]; + } + }, this); + } +}; + +MixpanelPersistence.prototype.load_queue = function(queue) { + return this.load_prop(this._get_queue_key(queue)); +}; + +MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === REMOVE_ACTION) { + return REMOVE_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console.error('Invalid queue:', queue); + } +}; + +MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + return this['props'][key] || (this['props'][key] = default_val); +}; + +MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); +}; + +MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; +}; + +/* eslint camelcase: "off" */ + +/* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @output_file_name mixpanel-2.8.min.js +// ==/ClosureCompiler== + +/* +SIMPLE STYLE GUIDE: + +this.x === public function +this._x === internal - only use within this file +this.__x === private - only use within the class + +Globals should be all caps +*/ + +var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + +var mixpanel_master; // main mixpanel instance / object +var INIT_MODULE = 0; +var INIT_SNIPPET = 1; + +var IDENTITY_FUNC = function(x) {return x;}; +var NOOP_FUNC = function() {}; + +/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; +/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; +/** @const */ var PAYLOAD_TYPE_JSON = 'json'; +/** @const */ var DEVICE_ID_PREFIX = '$device:'; + + +/* + * Dynamic... constants? Is that an oxymoron? + */ +// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ +// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials +var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + +// IE<10 does not support cross-origin XHR's but script tags +// with defer won't block window.onload; ENQUEUE_REQUESTS +// should only be true for Opera<12 +var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + +// save reference to navigator.sendBeacon so it can be minified +var sendBeacon = null; +if (navigator['sendBeacon']) { + sendBeacon = function() { + // late reference to navigator.sendBeacon to allow patching/spying + return navigator['sendBeacon'].apply(navigator, arguments); + }; +} + +var DEFAULT_API_ROUTES = { + 'track': 'track/', + 'engage': 'engage/', + 'groups': 'groups/', + 'record': 'record/' +}; + +/* + * Module-level globals + */ +var DEFAULT_CONFIG = { + 'api_host': 'https://api-js.mixpanel.com', + 'api_routes': DEFAULT_API_ROUTES, + 'api_method': 'POST', + 'api_transport': 'XHR', + 'api_payload_format': PAYLOAD_TYPE_BASE64, + 'app_host': 'https://mixpanel.com', + 'cdn': 'https://cdn.mxpnl.com', + 'cross_site_cookie': false, + 'cross_subdomain_cookie': true, + 'error_reporter': NOOP_FUNC, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_domain': '', + 'cookie_name': '', + 'loaded': NOOP_FUNC, + 'mp_loader': null, + 'track_marketing': true, + 'track_pageview': false, + 'skip_first_touch_marketing': false, + 'store_google': true, + 'stop_utm_persistence': false, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'opt_out_tracking_by_default': false, + 'opt_out_persistence_by_default': false, + 'opt_out_tracking_persistence_type': 'localStorage', + 'opt_out_tracking_cookie_prefix': null, + 'property_blacklist': [], + 'xhr_headers': {}, // { header: value, header2: value } + 'ignore_dnt': false, + 'batch_requests': true, + 'batch_size': 50, + 'batch_flush_interval_ms': 5000, + 'batch_request_timeout_ms': 90000, + 'batch_autostart': true, + 'hooks': {}, + 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), + 'record_block_selector': 'img, video', + 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), + 'record_mask_text_selector': '*', + 'record_max_ms': MAX_RECORDING_MS, + 'record_sessions_percent': 0, + 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' +}; + +var DOM_LOADED = false; + +/** + * Mixpanel Library Object + * @constructor + */ +var MixpanelLib = function() {}; + + +/** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ +var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._cached_groups = {}; // cache groups in a pool + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + if (!instance.get_config('skip_first_touch_marketing')) { + // We need null UTM params in the object because + // UTM parameters act as a tuple. If any UTM param + // is present, then we set all UTM params including + // empty ones together + var utm_params = _.info.campaignParams(null); + var initial_utm_params = {}; + var has_utm = false; + _.each(utm_params, function(utm_value, utm_key) { + initial_utm_params['initial_' + utm_key] = utm_value; + if (utm_value) { + has_utm = true; + } + }); + if (has_utm) { + instance['people'].set_once(initial_utm_params); + } + } + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; +}; + +// Initialization methods + +/** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ +MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + this.report_error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; +}; + +// mixpanel._init(token:string, config:object, name:string) +// +// This function sets up the current instance of the mixpanel +// library. The difference between this method and the init(...) +// method is this one initializes the actual instance, whereas the +// init(...) method sets up a new library and calls _init on it. +// +MixpanelLib.prototype._init = function(token, config, name) { + config = config || {}; + + this['__loaded'] = true; + this['config'] = {}; + + var variable_features = {}; + + // default to JSON payload for standard mixpanel.com API hosts + if (!('api_payload_format' in config)) { + var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; + if (api_host.match(/\.mixpanel\.com/)) { + variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; + } + } + + this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = NOOP_FUNC; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + // set up request queueing/batching + this.request_batchers = {}; + this._batch_requests = this.get_config('batch_requests'); + if (this._batch_requests) { + if (!_.localStorage.is_supported(true) || !USE_XHR) { + this._batch_requests = false; + console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + _.each(this.get_batcher_configs(), function(batcher_config) { + console.log('Clearing batch queue ' + batcher_config.queue_key); + _.localStorage.remove(batcher_config.queue_key); + }); + } else { + this.init_batchers(); + if (sendBeacon && win.addEventListener) { + // Before page closes or hides (user tabs away etc), attempt to flush any events + // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, + // events will not be removed from the persistent store; if the site is loaded again, + // the events will be flushed again on startup and deduplicated on the Mixpanel server + // side. + // There is no reliable way to capture only page close events, so we lean on the + // visibilitychange and pagehide events as recommended at + // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. + // These events fire when the user clicks away from the current page/tab, so will occur + // more frequently than page unload, but are the only mechanism currently for capturing + // this scenario somewhat reliably. + var flush_on_unload = _.bind(function() { + if (!this.request_batchers.events.stopped) { + this.request_batchers.events.flush({unloading: true}); + } + }, this); + win.addEventListener('pagehide', function(ev) { + if (ev['persisted']) { + flush_on_unload(); + } + }); + win.addEventListener('visibilitychange', function() { + if (document$1['visibilityState'] === 'hidden') { + flush_on_unload(); + } + }); + } + } + } + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.unpersisted_superprops = {}; + this._gdpr_init(); + + var uuid = _.UUID(); + if (!this.get_distinct_id()) { + // There is no need to set the distinct id + // or the device id if something was already stored + // in the persitence + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); + } + + var track_pageview_option = this.get_config('track_pageview'); + if (track_pageview_option) { + this._init_url_change_tracking(track_pageview_option); + } + + if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { + this.start_session_recording(); + } +}; + +MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { + if (!win['MutationObserver']) { + console.critical('Browser does not support MutationObserver; skipping session recording'); + return; + } + + var handleLoadedRecorder = _.bind(function() { + this._recorder = this._recorder || new win['__mp_recorder'](this); + this._recorder['startRecording'](); + }, this); + + if (_.isUndefined(win['__mp_recorder'])) { + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); + } else { + handleLoadedRecorder(); + } +}); + +MixpanelLib.prototype.stop_session_recording = function () { + if (this._recorder) { + this._recorder['stopRecording'](); + } else { + console.critical('Session recorder module not loaded'); + } +}; + +MixpanelLib.prototype.get_session_recording_properties = function () { + var props = {}; + if (this._recorder) { + var replay_id = this._recorder['replayId']; + if (replay_id) { + props['$mp_replay_id'] = replay_id; + } + } + return props; +}; + +// Private methods + +MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + this._set_default_superprops(); + this['people'].set_once(this['persistence'].get_referrer_info()); + + // `store_google` is now deprecated and previously stored UTM parameters are cleared + // from persistence by default. + if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { + var utm_params = _.info.campaignParams(null); + _.each(utm_params, function(_utm_value, utm_key) { + // We need to unregister persisted UTM parameters so old values + // are not mixed with the new UTM parameters + this.unregister(utm_key); + }.bind(this)); + } +}; + +// update persistence with info on referrer, UTM params, etc +MixpanelLib.prototype._set_default_superprops = function() { + this['persistence'].update_search_keyword(document$1.referrer); + // Registering super properties for UTM persistence by 'store_google' is deprecated. + if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { + this.register(_.info.campaignParams()); + } + if (this.get_config('save_referrer')) { + this['persistence'].update_referrer_info(document$1.referrer); + } +}; + +MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + + if (!this.has_opted_out_tracking()) { + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + } + + delete this.__dom_loaded_queue; + delete this.__request_queue; +}; + +MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + this.report_error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); +}; + +MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { + var previous_tracked_url = ''; + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = _.info.currentUrl(); + } + + if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { + win.addEventListener('popstate', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + win.addEventListener('hashchange', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + var nativePushState = win.history.pushState; + if (typeof nativePushState === 'function') { + win.history.pushState = function(state, unused, url) { + nativePushState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + var nativeReplaceState = win.history.replaceState; + if (typeof nativeReplaceState === 'function') { + win.history.replaceState = function(state, unused, url) { + nativeReplaceState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + win.addEventListener('mp_locationchange', function() { + var current_url = _.info.currentUrl(); + var should_track = false; + if (track_pageview_option === 'full-url') { + should_track = current_url !== previous_tracked_url; + } else if (track_pageview_option === 'url-with-path-and-query-string') { + should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; + } else if (track_pageview_option === 'url-with-path') { + should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; + } + + if (should_track) { + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = current_url; + } + } + }.bind(this)); + } +}; + +/** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ +MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } +}; + +MixpanelLib.prototype._send_request = function(url, data, options, callback) { + var succeeded = true; + + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return succeeded; + } + + var DEFAULT_OPTIONS = { + method: this.get_config('api_method'), + transport: this.get_config('api_transport'), + verbose: this.get_config('verbose') + }; + var body_data = null; + + if (!callback && (_.isFunction(options) || typeof options === 'string')) { + callback = options; + options = null; + } + options = _.extend(DEFAULT_OPTIONS, options || {}); + if (!USE_XHR) { + options.method = 'GET'; + } + var use_post = options.method === 'POST'; + var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; + + // needed to correctly format responses + var verbose_mode = options.verbose; + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + + if (use_post) { + body_data = 'data=' + encodeURIComponent(data['data']); + delete data['data']; + } + + url += '?' + _.HTTPBuildQuery(data); + + var lib = this; + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (use_sendBeacon) { + try { + succeeded = sendBeacon(url, body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + try { + if (callback) { + callback(succeeded ? 1 : 0); + } + } catch (e) { + lib.report_error(e); + } + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + + var headers = this.get_config('xhr_headers'); + if (use_post) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + + return succeeded; +}; + +/** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ +MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (_.isArray(fn_name)) { + tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() + } else if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + if (_.isArray(item[0])) { + // chained call + var caller = context; + _.each(item, function(call) { + caller = caller[call[0]].apply(caller, call.slice(1)); + }); + } else { + this[item[0]].apply(this, item.slice(1)); + } + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); +}; + +// request queueing utils + +MixpanelLib.prototype.are_batchers_initialized = function() { + return !!this.request_batchers.events; +}; + +MixpanelLib.prototype.get_batcher_configs = function() { + var queue_prefix = '__mpq_' + this.get_config('token'); + var api_routes = this.get_config('api_routes'); + this._batcher_configs = this._batcher_configs || { + events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, + people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, + groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} + }; + return this._batcher_configs; +}; + +MixpanelLib.prototype.init_batchers = function() { + if (!this.are_batchers_initialized()) { + var batcher_for = _.bind(function(attrs) { + return new RequestBatcher( + attrs.queue_key, + { + libConfig: this['config'], + sendRequestFunc: _.bind(function(data, options, cb) { + this._send_request( + this.get_config('api_host') + attrs.endpoint, + this._encode_data_for_request(data), + options, + this._prepare_callback(cb, data) + ); + }, this), + beforeSendHook: _.bind(function(item) { + return this._run_hook('before_send_' + attrs.type, item); + }, this), + errorReporter: this.get_config('error_reporter'), + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + } + ); + }, this); + var batcher_configs = this.get_batcher_configs(); + this.request_batchers = { + events: batcher_for(batcher_configs.events), + people: batcher_for(batcher_configs.people), + groups: batcher_for(batcher_configs.groups) + }; + } + if (this.get_config('batch_autostart')) { + this.start_batch_senders(); + } +}; + +MixpanelLib.prototype.start_batch_senders = function() { + this._batchers_were_started = true; + if (this.are_batchers_initialized()) { + this._batch_requests = true; + _.each(this.request_batchers, function(batcher) { + batcher.start(); + }); + } +}; + +MixpanelLib.prototype.stop_batch_senders = function() { + this._batch_requests = false; + _.each(this.request_batchers, function(batcher) { + batcher.stop(); + batcher.clear(); + }); +}; + +/** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ +MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); +}; + +/** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ +MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } +}; + +MixpanelLib.prototype._encode_data_for_request = function(data) { + var encoded_data = _.JSONEncode(data); + if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { + encoded_data = _.base64Encode(encoded_data); + } + return {'data': encoded_data}; +}; + +// internal method for handling track vs batch-enqueue logic +MixpanelLib.prototype._track_or_batch = function(options, callback) { + var truncated_data = _.truncate(options.data, 255); + var endpoint = options.endpoint; + var batcher = options.batcher; + var should_send_immediately = options.should_send_immediately; + var send_request_options = options.send_request_options || {}; + callback = callback || NOOP_FUNC; + + var request_enqueued_or_initiated = true; + var send_request_immediately = _.bind(function() { + if (!send_request_options.skip_hooks) { + truncated_data = this._run_hook('before_send_' + options.type, truncated_data); + } + if (truncated_data) { + console.log('MIXPANEL REQUEST:'); + console.log(truncated_data); + return this._send_request( + endpoint, + this._encode_data_for_request(truncated_data), + send_request_options, + this._prepare_callback(callback, truncated_data) + ); + } else { + return null; + } + }, this); + + if (this._batch_requests && !should_send_immediately) { + batcher.enqueue(truncated_data, function(succeeded) { + if (succeeded) { + callback(1, truncated_data); + } else { + send_request_immediately(); + } + }); + } else { + request_enqueued_or_initiated = send_request_immediately(); + } + + return request_enqueued_or_initiated && truncated_data; +}; + +/** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // track an event using navigator.sendBeacon + * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this track request. + * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). + * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + var transport = options['transport']; // external API, don't minify 'transport' prop + if (transport) { + options.transport = transport; // 'transport' prop name can be minified internally + } + var should_send_immediately = options['send_immediately']; + if (typeof callback !== 'function') { + callback = NOOP_FUNC; + } + + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = _.extend({}, properties); + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + this._set_default_superprops(); + + var marketing_properties = this.get_config('track_marketing') + ? _.info.marketingParams() + : {}; + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties({'mp_loader': this.get_config('mp_loader')}), + marketing_properties, + this['persistence'].properties(), + this.unpersisted_superprops, + this.get_session_recording_properties(), + properties + ); + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + var ret = this._track_or_batch({ + type: 'events', + data: data, + endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], + batcher: this.request_batchers.events, + should_send_immediately: should_send_immediately, + send_request_options: options + }, callback); + + return ret; +}); + +/** + * Register the current user into one/many groups. + * + * ### Usage: + * + * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs + * mixpanel.set_group('company', 'mixpanel') + * mixpanel.set_group('company', 128746312) + * + * @param {String} group_key Group key + * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * + */ +MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { + if (!_.isArray(group_ids)) { + group_ids = [group_ids]; + } + var prop = {}; + prop[group_key] = group_ids; + this.register(prop); + return this['people'].set(group_key, group_ids, callback); +}); + +/** + * Add a new group for this user. + * + * ### Usage: + * + * mixpanel.add_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_values = this.get_property(group_key); + var prop = {}; + if (old_values === undefined) { + prop[group_key] = [group_id]; + this.register(prop); + } else { + if (old_values.indexOf(group_id) === -1) { + old_values.push(group_id); + prop[group_key] = old_values; + this.register(prop); + } + } + return this['people'].union(group_key, group_id, callback); +}); + +/** + * Remove a group from this user. + * + * ### Usage: + * + * mixpanel.remove_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_value = this.get_property(group_key); + // if the value doesn't exist, the persistent store is unchanged + if (old_value !== undefined) { + var idx = old_value.indexOf(group_id); + if (idx > -1) { + old_value.splice(idx, 1); + this.register({group_key: old_value}); + } + if (old_value.length === 0) { + this.unregister(group_key); + } + } + return this['people'].remove(group_key, group_id, callback); +}); + +/** + * Track an event with specific groups. + * + * ### Usage: + * + * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) + * + * @param {String} event_name The name of the event (see `mixpanel.track()`) + * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) + * @param {Object=} groups An object mapping group name keys to one or more values + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { + var tracking_props = _.extend({}, properties || {}); + _.each(groups, function(v, k) { + if (v !== null && v !== undefined) { + tracking_props[k] = v; + } + }); + return this.track(event_name, tracking_props, callback); +}); + +MixpanelLib.prototype._create_map_key = function (group_key, group_id) { + return group_key + '_' + JSON.stringify(group_id); +}; + +MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { + delete this._cached_groups[this._create_map_key(group_key, group_id)]; +}; + +/** + * Look up reference to a Mixpanel group + * + * ### Usage: + * + * mixpanel.get_group(group_key, group_id) + * + * @param {String} group_key Group key + * @param {Object} group_id A valid Mixpanel property type + * @returns {Object} A MixpanelGroup identifier + */ +MixpanelLib.prototype.get_group = function (group_key, group_id) { + var map_key = this._create_map_key(group_key, group_id); + var group = this._cached_groups[map_key]; + if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { + group = new MixpanelGroup(); + group._init(this, group_key, group_id); + this._cached_groups[map_key] = group; + } + return group; +}; + +/** + * Track a default Mixpanel page view event, which includes extra default event properties to + * improve page view data. + * + * ### Usage: + * + * // track a default $mp_web_page_view event + * mixpanel.track_pageview(); + * + * // track a page view event with additional event properties + * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); + * + * // example approach to track page views on different page types as event properties + * mixpanel.track_pageview({'page': 'pricing'}); + * mixpanel.track_pageview({'page': 'homepage'}); + * + * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for + * // individual pages on the same site or product. Use cases for custom event_name may be page + * // views on different products or internal applications that are considered completely separate + * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); + * + * ### Notes: + * + * The `config.track_pageview` option for mixpanel.init() + * may be turned on for tracking page loads automatically. + * + * // track only page loads + * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); + * + * // track when the URL changes in any manner + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); + * + * // track when the URL changes, ignoring any changes in the hash part + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); + * + * // track when the path changes, ignoring any query parameter or hash changes + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); + * + * @param {Object} [properties] An optional set of additional properties to send with the page view event + * @param {Object} [options] Page view tracking options + * @param {String} [options.event_name] - Alternate name for the tracking event + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { + if (typeof properties !== 'object') { + properties = {}; + } + options = options || {}; + var event_name = options['event_name'] || '$mp_web_page_view'; + + var default_page_properties = _.extend( + _.info.mpPageViewProperties(), + _.info.campaignParams(), + _.info.clickParams() + ); + + var event_properties = _.extend( + {}, + default_page_properties, + properties + ); + + return this.track(event_name, event_properties); +}); + +/** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ +MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); +}; + +/** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ +MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); +}; + +/** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ +MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); +}; + +var REGISTER_DEFAULTS = { + 'persistent': true +}; +/** + * Helper to parse options param for register methods, maintaining + * legacy support for plain "days" param instead of options object + * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods + * @returns {Object} options object + */ +var options_for_register = function(days_or_options) { + var options; + if (_.isObject(days_or_options)) { + options = days_or_options; + } else if (!_.isUndefined(days_or_options)) { + options = {'days': days_or_options}; + } else { + options = {}; + } + return _.extend({}, REGISTER_DEFAULTS, options); +}; + +/** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * // register only for the current pageload + * mixpanel.register({'Name': 'Pat'}, {persistent: false}); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register = function(props, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register(props, options['days']); + } else { + _.extend(this.unpersisted_superprops, props); + } +}; + +/** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * // register once, only for the current pageload + * mixpanel.register_once({ + * 'First interaction time': new Date().toISOString() + * }, 'None', {persistent: false}); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register_once(props, default_value, options['days']); + } else { + if (typeof(default_value) === 'undefined') { + default_value = 'None'; + } + _.each(props, function(val, prop) { + if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { + this.unpersisted_superprops[prop] = val; + } + }, this); + } +}; + +/** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + * @param {Object} [options] + * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.unregister = function(property, options) { + options = options_for_register(options); + if (options['persistent']) { + this['persistence'].unregister(property); + } else { + delete this.unpersisted_superprops[property]; + } +}; + +MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); +}; + +/** + * Identify a user with a unique ID to track user activity across + * devices, tie a user to their events, and create a user profile. + * If you never call this method, unique visitors are tracked using + * a UUID generated the first time they visit the site. + * + * Call identify when you know the identity of the current user, + * typically after login or signup. We recommend against using + * identify for anonymous visitors to your site. + * + * ### Notes: + * If your project has + * ID Merge + * enabled, the identify method will connect pre- and + * post-authentication events when appropriate. + * + * If your project does not have ID Merge enabled, identify will + * change the user's local distinct_id to the unique ID you pass. + * Events tracked prior to authentication will not be connected + * to the same user identity. If ID Merge is disabled, alias can + * be used to connect pre- and post-registration events. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ +MixpanelLib.prototype.identify = function( + new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + var previous_distinct_id = this.get_distinct_id(); + if (new_distinct_id && previous_distinct_id !== new_distinct_id) { + // we allow the following condition if previous distinct_id is same as new_distinct_id + // so that you can force flush people updates for anonymous profiles. + if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { + this.report_error('distinct_id cannot have $device: prefix'); + return -1; + } + this.register({'$user_id': new_distinct_id}); + } + + if (!this.get_property('$device_id')) { + // The persisted distinct id might not actually be a device id at all + // it might be a distinct id of the user from before + var device_id = previous_distinct_id; + this.register_once({ + '$had_persisted_distinct_id': true, + '$device_id': device_id + }, ''); + } + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this.register({'distinct_id': new_distinct_id}); + } + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); + + // send an $identify event any time the distinct_id is changing - logic on the server + // will determine whether or not to do anything with it. + if (new_distinct_id !== previous_distinct_id) { + this.track('$identify', { + 'distinct_id': new_distinct_id, + '$anon_distinct_id': previous_distinct_id + }, {skip_hooks: true}); + } +}; + +/** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ +MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + var uuid = _.UUID(); + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); +}; + +/** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ +MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); +}; + +/** + * The alias method creates an alias which Mixpanel will use to + * remap one id to another. Multiple aliases can point to the + * same identifier. + * + * The following is a valid use of alias: + * + * mixpanel.alias('new_id', 'existing_id'); + * // You can add multiple id aliases to the existing ID + * mixpanel.alias('newer_id', 'existing_id'); + * + * Aliases can also be chained - the following is a valid example: + * + * mixpanel.alias('new_id', 'existing_id'); + * // chain newer_id - new_id - existing_id + * mixpanel.alias('newer_id', 'new_id'); + * + * Aliases cannot point to multiple identifiers - the following + * example will not work: + * + * mixpanel.alias('new_id', 'existing_id'); + * // this is invalid as 'new_id' already points to 'existing_id' + * mixpanel.alias('new_id', 'newer_id'); + * + * ### Notes: + * + * If your project does not have + * ID Merge + * enabled, the best practice is to call alias once when a unique + * ID is first created for a user (e.g., when a user first registers + * for an account). Do not use alias multiple times for a single + * user without ID Merge enabled. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ +MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + this.report_error('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { + 'alias': alias, + 'distinct_id': original + }, { + skip_hooks: true + }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + this.report_error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } +}; + +/** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @deprecated + */ +MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); +}; + +/** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // host for requests (customizable for e.g. a local proxy) + * api_host: 'https://api-js.mixpanel.com', + * + * // endpoints for different types of requests + * api_routes: { + * track: 'track/', + * engage: 'engage/', + * groups: 'groups/', + * } + * + * // HTTP method for tracking requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. Mixpanel + * // tracking via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // request-batching/queueing/retry + * batch_requests: true, + * + * // maximum number of events/updates to send in a single + * // network request + * batch_size: 50, + * + * // milliseconds to wait between sending batch requests + * batch_flush_interval_ms: 5000, + * + * // milliseconds to wait for network responses to batch requests + * // before they are considered timed-out and retried + * batch_request_timeout_ms: 90000, + * + * // override value for cookie domain, only useful for ensuring + * // correct cross-subdomain cookies on unusual domains like + * // subdomain.mainsite.avocat.fr; NB this cannot be used to + * // set cookies on a different domain than the current origin + * cookie_domain: '' + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // if true, cookie will be set with SameSite=None; Secure + * // this is only useful in special situations, like embedded + * // 3rd-party iframes that set up a Mixpanel instance + * cross_site_cookie: false + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of tracking by this Mixpanel instance by default + * opt_out_tracking_by_default: false + * + * // opt users out of browser data storage by this Mixpanel instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_tracking_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_tracking_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // disables enriching user profiles with first touch marketing data + * skip_first_touch_marketing: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // adds any UTM parameters and click IDs present on the page to any events fired + * track_marketing: true + * + * // enables automatic page view tracking using default page view events through + * // the track_pageview() method + * track_pageview: false + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // whether to ignore or respect the web browser's Do Not Track setting + * ignore_dnt: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ +MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + var new_batch_size = config['batch_size']; + if (new_batch_size) { + _.each(this.request_batchers, function(batcher) { + batcher.resetBatchSize(); + }); + } + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } +}; + +/** + * returns the current config object for the library. + */ +MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; +}; + +/** + * Fetch a hook function from config, with safe default, and run it + * against the given arguments + * @param {string} hook_name which hook to retrieve + * @returns {any|null} return value of user-provided hook, or null if nothing was returned + */ +MixpanelLib.prototype._run_hook = function(hook_name) { + var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); + if (typeof ret === 'undefined') { + this.report_error(hook_name + ' hook did not return a value'); + ret = null; + } + return ret; +}; + +/** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ +MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence'].load_prop([property_name]); +}; + +MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; +}; + +MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); +}; + +// perform some housekeeping around GDPR opt-in/out state +MixpanelLib.prototype._gdpr_init = function() { + var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; + + // try to convert opt-in/out cookies to localStorage if possible + if (is_localStorage_requested && _.localStorage.is_supported()) { + if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { + this.opt_in_tracking({'enable_persistence': false}); + } + if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { + this.opt_out_tracking({'clear_persistence': false}); + } + this.clear_opt_in_out_tracking({ + 'persistence_type': 'cookie', + 'enable_persistence': false + }); + } + + // check whether the user has already opted out - if so, clear & disable persistence + if (this.has_opted_out_tracking()) { + this._gdpr_update_persistence({'clear_persistence': true}); + + // check whether we should opt out by default + // note: we don't clear persistence here by default since opt-out default state is often + // used as an initial state while GDPR information is being collected + } else if (!this.has_opted_in_tracking() && ( + this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') + )) { + _.cookie.remove('mp_optout'); + this.opt_out_tracking({ + 'clear_persistence': this.get_config('opt_out_persistence_by_default') + }); + } +}; + +/** + * Enable or disable persistence based on options + * only enable/disable if persistence is not already in this state + * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it + * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence + */ +MixpanelLib.prototype._gdpr_update_persistence = function(options) { + var disabled; + if (options && options['clear_persistence']) { + disabled = true; + } else if (options && options['enable_persistence']) { + disabled = false; + } else { + return; + } + + if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { + this['persistence'].set_disabled(disabled); + } + + if (disabled) { + this.stop_batch_senders(); + } else { + // only start batchers after opt-in if they have previously been started + // in order to avoid unintentionally starting up batching for the first time + if (this._batchers_were_started) { + this.start_batch_senders(); + } + } +}; + +// call a base gdpr function after constructing the appropriate token and options args +MixpanelLib.prototype._gdpr_call_func = function(func, options) { + options = _.extend({ + 'track': _.bind(this.track, this), + 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), + 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), + 'cookie_expiration': this.get_config('cookie_expiration'), + 'cross_site_cookie': this.get_config('cross_site_cookie'), + 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), + 'cookie_domain': this.get_config('cookie_domain'), + 'secure_cookie': this.get_config('secure_cookie'), + 'ignore_dnt': this.get_config('ignore_dnt') + }, options); + + // check if localStorage can be used for recording opt out status, fall back to cookie if not + if (!_.localStorage.is_supported()) { + options['persistence_type'] = 'cookie'; + } + + return func(this.get_config('token'), { + track: options['track'], + trackEventName: options['track_event_name'], + trackProperties: options['track_properties'], + persistenceType: options['persistence_type'], + persistencePrefix: options['cookie_prefix'], + cookieDomain: options['cookie_domain'], + cookieExpiration: options['cookie_expiration'], + crossSiteCookie: options['cross_site_cookie'], + crossSubdomainCookie: options['cross_subdomain_cookie'], + secureCookie: options['secure_cookie'], + ignoreDnt: options['ignore_dnt'] + }); +}; + +/** + * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user in + * mixpanel.opt_in_tracking(); + * + * // opt user in with specific event name, properties, cookie configuration + * mixpanel.opt_in_tracking({ + * track_event_name: 'User opted in', + * track_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) + * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action + * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_in_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(optIn, options); + this._gdpr_update_persistence(options); +}; + +/** + * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user out + * mixpanel.opt_out_tracking(); + * + * // opt user out with different cookie configuration from Mixpanel instance + * mixpanel.opt_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_out_tracking = function(options) { + options = _.extend({ + 'clear_persistence': true, + 'delete_user': true + }, options); + + // delete user and clear charges since these methods may be disabled by opt-out + if (options['delete_user'] && this['people'] && this['people']._identify_called()) { + this['people'].delete_user(); + this['people'].clear_charges(); + } + + this._gdpr_call_func(optOut, options); + this._gdpr_update_persistence(options); +}; + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_in = mixpanel.has_opted_in_tracking(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ +MixpanelLib.prototype.has_opted_in_tracking = function(options) { + return this._gdpr_call_func(hasOptedIn, options); +}; + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_out = mixpanel.has_opted_out_tracking(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ +MixpanelLib.prototype.has_opted_out_tracking = function(options) { + return this._gdpr_call_func(hasOptedOut, options); +}; + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // clear user's opt-in/out status + * mixpanel.clear_opt_in_out_tracking(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_tracking/opt_out_tracking methods were called. + * mixpanel.clear_opt_in_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(clearOptInOut, options); + this._gdpr_update_persistence(options); +}; + +MixpanelLib.prototype.report_error = function(msg, err) { + console.error.apply(console.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + console.error(err); + } +}; + +// EXPORTS (for closure compiler) + +// MixpanelLib Exports +MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; +MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; +MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; +MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; +MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; +MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; +MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; +MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; +MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; +MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; +MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; +MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; +MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; +MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; +MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; +MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; +MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; +MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; +MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; +MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; +MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; +MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; +MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; +MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; +MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; +MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; +MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; +MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; +MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; +MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; +MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; +MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; +MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; +MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; +MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; + +// MixpanelPersistence Exports +MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; +MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; +MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; +MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; +MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + +var instances = {}; +var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; +}; + +var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; +}; + +var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); +}; + +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; + init_type = INIT_MODULE; + mixpanel_master = new MixpanelLib(); + + override_mp_init_func(); + mixpanel_master['init'](); + add_dom_loaded_handler(); + + return mixpanel_master; +} + +// For loading separate bundles asynchronously via script tag + +// For builds that do NOT want any extra bundles (e.g. session recorder) +// and just the main SDK, throw an error when trying to load a separate bundle. +// eslint-disable-next-line no-unused-vars +function loadThrowError (src, _onload) { + throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); +} + +/* eslint camelcase: "off" */ + +var mixpanel = init_as_module(loadThrowError); + +module.exports = mixpanel; diff --git a/package.json b/package.json index e69741ad..04ccb6dd 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "request": "2.88.0", "rollup": "2.79.1", "rollup-plugin-esbuild": "4.10.3", - "rollup-plugin-npm": "1.4.0", "sinon": "8.1.1", "sinon-chai": "3.5.0", "webpack": "1.12.2" diff --git a/rollup.config.js b/rollup.config.js index 709c1709..42e72c0c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,8 +1,8 @@ -import npm from 'rollup-plugin-npm'; +import nodeResolve from '@rollup/plugin-node-resolve'; export default { plugins: [ - npm({ + nodeResolve({ browser: true, main: true, jsnext: true, diff --git a/src/loaders/loader-module.js b/src/loaders/loader-module.js index 630cf697..f7ba005c 100644 --- a/src/loaders/loader-module.js +++ b/src/loaders/loader-module.js @@ -1,5 +1,5 @@ /* eslint camelcase: "off" */ -import '../../dist/mixpanel-recorder'; +import '../recorder'; import { init_as_module } from '../mixpanel-core'; import { loadNoop } from './bundle-loaders'; diff --git a/src/recorder/index.js b/src/recorder/index.js index dd573a65..59a08f85 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -1,6 +1,6 @@ -import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js'; +import { record } from 'rrweb'; -import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase +import { MAX_RECORDING_MS, console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase import { addOptOutCheckMixpanelLib } from '../gdpr-utils'; var logger = console_with_prefix('recorder'); diff --git a/tests/unit/loader.js b/tests/unit/loader.js index 228d7b06..386d4149 100644 --- a/tests/unit/loader.js +++ b/tests/unit/loader.js @@ -5,17 +5,9 @@ * currently not supported in the browser lib). */ -import jsDomSetup from './jsdom-setup'; +import mixpanel from '../../src/loaders/loader-module'; describe(`Module-based loader in Node env`, function() { - let mixpanel; - jsDomSetup({ - reImportModules: [`../../src/loaders/loader-module`], - beforeCallback: function(modules) { - mixpanel = modules[0]; - } - }); - it(`supports init() with options`, function(done) { mixpanel.init(`test-token`, { debug: true, From 2abd9349f0a45f4a26c13c78bb95a7383a00e216 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 16 Jul 2024 09:12:56 -0500 Subject: [PATCH 39/48] Update src/recorder/index.js Co-authored-by: teddddd --- src/recorder/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/recorder/index.js b/src/recorder/index.js index f41e0839..4903c272 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -1,5 +1,6 @@ import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js'; import {IncrementalSource, EventType} from '@rrweb/types'; + import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase import { addOptOutCheckMixpanelLib } from '../gdpr-utils'; From 77e0087aac16fa4d2c360b03d695af165a1aa07d Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 17 Jul 2024 17:21:56 +0000 Subject: [PATCH 40/48] review feedback --- .vscode/launch.json | 23 + README.md | 4 +- build.sh | 4 +- dist/mixpanel-core.cjs.js | 6330 ++++++++++++++++ dist/mixpanel-with-async-recorder.cjs.js | 6332 +++++++++++++++++ rollup.config.js | 2 +- src/loaders/bundle-loaders.js | 2 +- ...r-module-main.js => loader-module-core.js} | 0 ...s => loader-module-with-async-recorder.js} | 0 9 files changed, 12691 insertions(+), 6 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 dist/mixpanel-core.cjs.js create mode 100644 dist/mixpanel-with-async-recorder.cjs.js rename src/loaders/{loader-module-main.js => loader-module-core.js} (100%) rename src/loaders/{loader-module-bundle-async.js => loader-module-with-async-recorder.js} (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..0cc9863e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Mocha (Test single file)", + "type": "node", + "request": "launch", + "env": { + "BABEL_ENV": "test" + }, + "runtimeArgs": [ + "--require", + "babel-core/register", + "${workspaceRoot}/node_modules/.bin/mocha", + "--inspect-brk", + "${relativeFile}", + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "port": 9229 + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index b746818e..d4c1e48b 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,12 @@ NOTE: the default `mixpanel-browser` bundle includes a bundled `mixpanel-recorde To load the main SDK with no option of session recording: ```javascript -var mixpanel = require('mixpanel-browser/dist/mixpanel-main.cjs') +import mixpanel from 'mixpanel-browser/src/loaders/loader-module-core'; ``` To load the main SDK and optionally load session recording bundle asynchronously (via script tag): ```javascript -var mixpanel = require('mixpanel-browser/dist/mixpanel-bundle-async.cjs') +import mixpanel from 'mixpanel-browser/src/loaders/loader-module-with-async-recorder'; ``` ## Alternative installation via Bower diff --git a/build.sh b/build.sh index 1ae3d16c..c92bbb07 100755 --- a/build.sh +++ b/build.sh @@ -27,8 +27,8 @@ if [ ! -z "$FULL" ]; then echo 'Building module bundles' npx rollup -i src/loaders/loader-module.js -f amd -o build/mixpanel.amd.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f cjs -o build/mixpanel.cjs.js -c rollup.config.js - npx rollup -i src/loaders/loader-module-main.js -f cjs -o build/mixpanel-main.cjs.js -c rollup.config.js - npx rollup -i src/loaders/loader-module-bundle-async.js -f cjs -o build/mixpanel-bundle-async.cjs.js -c rollup.config.js + npx rollup -i src/loaders/loader-module-core.js -f cjs -o build/mixpanel-core.cjs.js -c rollup.config.js + npx rollup -i src/loaders/loader-module-with-async-recorder.js -f cjs -o build/mixpanel-with-async-recorder.cjs.js -c rollup.config.js npx rollup -i src/loaders/loader-module.js -f umd -o build/mixpanel.umd.js -n mixpanel -c rollup.config.js echo 'Bundling module-loader test runners' diff --git a/dist/mixpanel-core.cjs.js b/dist/mixpanel-core.cjs.js new file mode 100644 index 00000000..ba6f5a8b --- /dev/null +++ b/dist/mixpanel-core.cjs.js @@ -0,0 +1,6330 @@ +'use strict'; + +var Config = { + DEBUG: false, + LIB_VERSION: '2.53.0' +}; + +/* eslint camelcase: "off", eqeqeq: "off" */ + +// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file +var win; +if (typeof(window) === 'undefined') { + var loc = { + hostname: '' + }; + win = { + navigator: { userAgent: '' }, + document: { + location: loc, + referrer: '' + }, + screen: { width: 0, height: 0 }, + location: loc + }; +} else { + win = window; +} + +// Maximum allowed session recording length +var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours + +/* + * Saved references to long variable names, so that closure compiler can + * minimize file size. + */ + +var ArrayProto = Array.prototype, + FuncProto = Function.prototype, + ObjProto = Object.prototype, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty, + windowConsole = win.console, + navigator = win.navigator, + document$1 = win.document, + windowOpera = win.opera, + screen = win.screen, + userAgent = navigator.userAgent; + +var nativeBind = FuncProto.bind, + nativeForEach = ArrayProto.forEach, + nativeIndexOf = ArrayProto.indexOf, + nativeMap = ArrayProto.map, + nativeIsArray = Array.isArray, + breaker = {}; + +var _ = { + trim: function(str) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } +}; + +// Console override +var console = { + /** @type {function(...*)} */ + log: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } + }, + /** @type {function(...*)} */ + warn: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); + try { + windowConsole.warn.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.warn(arg); + }); + } + } + }, + /** @type {function(...*)} */ + error: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + }, + /** @type {function(...*)} */ + critical: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + } +}; + +var log_func_with_prefix = function(func, prefix) { + return function() { + arguments[0] = '[' + prefix + '] ' + arguments[0]; + return func.apply(console, arguments); + }; +}; +var console_with_prefix = function(prefix) { + return { + log: log_func_with_prefix(console.log, prefix), + error: log_func_with_prefix(console.error, prefix), + critical: log_func_with_prefix(console.critical, prefix) + }; +}; + + +// UNDERSCORE +// Embed part of the Underscore Library +_.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + if (!_.isFunction(func)) { + throw new TypeError(); + } + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) { + return func.apply(context, args.concat(slice.call(arguments))); + } + var ctor = {}; + ctor.prototype = func.prototype; + var self = new ctor(); + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; +}; + +/** + * @param {*=} obj + * @param {function(...*)=} iterator + * @param {Object=} context + */ +_.each = function(obj, iterator, context) { + if (obj === null || obj === undefined) { + return; + } + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { + return; + } + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) { + return; + } + } + } + } +}; + +_.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; +}; + +_.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; +}; + +// from a comment on http://dbj.org/dbj/?p=286 +// fails on only one very rare and deliberate custom object: +// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; +_.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } +}; + +_.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); +}; + +_.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); +}; + +_.map = function(arr, callback, context) { + if (nativeMap && arr.map === nativeMap) { + return arr.map(callback, context); + } else { + var results = []; + _.each(arr, function(item) { + results.push(callback.call(context, item)); + }); + return results; + } +}; + +_.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; +}; + +_.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; +}; + +_.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; +}; + +_.includes = function(str, needle) { + return str.indexOf(needle) !== -1; +}; + +// Underscore Addons +_.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; +}; + +_.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); +}; + +_.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; +}; + +_.isUndefined = function(obj) { + return obj === void 0; +}; + +_.isString = function(obj) { + return toString.call(obj) == '[object String]'; +}; + +_.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; + +_.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; +}; + +_.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); +}; + +_.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; +}; + +_.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); +}; + +_.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); +}; + +_.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; +}; + +/* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ +_.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; +}; + +_.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; +})(); + +/** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ +_.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; +})(); + +_.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; +}; + +_.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; +}; + +_.UUID = (function() { + + // Time-based entropy + var T = function() { + var time = 1 * new Date(); // cross-browser version of Date.now() + var ticks; + if (win.performance && win.performance.now) { + ticks = win.performance.now(); + } else { + // fall back to busy loop + ticks = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (time == 1 * new Date()) { + ticks++; + } + } + return time.toString(16) + Math.floor(ticks).toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; +})(); + +// _.isBlockedUA() +// This is to block various web spiders from executing our JS and +// sending false tracking data +var BLOCKED_UA_STRS = [ + 'ahrefsbot', + 'ahrefssiteaudit', + 'baiduspider', + 'bingbot', + 'bingpreview', + 'chrome-lighthouse', + 'facebookexternal', + 'petalbot', + 'pinterest', + 'screaming frog', + 'yahoo! slurp', + 'yandexbot', + + // a whole bunch of goog-specific crawlers + // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers + 'adsbot-google', + 'apis-google', + 'duplexweb-google', + 'feedfetcher-google', + 'google favicon', + 'google web preview', + 'google-read-aloud', + 'googlebot', + 'googleweblight', + 'mediapartners-google', + 'storebot-google' +]; +_.isBlockedUA = function(ua) { + var i; + ua = ua.toLowerCase(); + for (i = 0; i < BLOCKED_UA_STRS.length; i++) { + if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { + return true; + } + } + return false; +}; + +/** + * @param {Object=} formdata + * @param {string=} arg_separator + */ +_.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); +}; + +_.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + var result = results[1]; + try { + result = decodeURIComponent(result); + } catch(err) { + console.error('Skipping decoding for malformed query param: ' + result); + } + return result.replace(/\+/g, ' '); + } +}; + + +// _.cookie +// Methods partially borrowed from quirksmode.org/js/cookies.html +_.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', + expires = '', + secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', expires = '', secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, is_cross_subdomain, domain_override) { + _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); + } +}; + +var _localStorageSupported = null; +var localStorageSupported = function(storage, forceCheck) { + if (_localStorageSupported !== null && !forceCheck) { + return _localStorageSupported; + } + + var supported = true; + try { + storage = storage || window.localStorage; + var key = '__mplss_' + cheap_guid(8), + val = 'xyz'; + storage.setItem(key, val); + if (storage.getItem(key) !== val) { + supported = false; + } + storage.removeItem(key); + } catch (err) { + supported = false; + } + + _localStorageSupported = supported; + return supported; +}; + +// _.localStorage +_.localStorage = { + is_supported: function(force_check) { + var supported = localStorageSupported(null, force_check); + if (!supported) { + console.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }, + + error: function(msg) { + console.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } +}; + +_.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...*)} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; +})(); + + +var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); + +_.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(TOKEN_MATCH_REGEX); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; +})(); + +var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; +var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; + +_.info = { + campaignParams: function(default_value) { + var kw = '', + params = {}; + _.each(CAMPAIGN_KEYWORDS, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } else if (default_value !== undefined) { + params[kwkey] = default_value; + } + }); + + return params; + }, + + clickParams: function() { + var id = '', + params = {}; + _.each(CLICK_IDS, function(idkey) { + id = _.getQueryParam(document$1.URL, idkey); + if (id.length) { + params[idkey] = id; + } + }); + + return params; + }, + + marketingParams: function() { + return _.extend(_.info.campaignParams(), _.info.clickParams()); + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'SamsungBrowser/')) { + // https://developer.samsung.com/internet/user-agent-string-format + return 'Samsung Internet'; + } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + currentUrl: function() { + return win.location.href; + }, + + properties: function(extra_props) { + if (typeof extra_props !== 'object') { + extra_props = {}; + } + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': _.info.currentUrl(), + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION, + '$insert_id': cheap_guid(), + 'time': _.timestamp() / 1000 // epoch time in seconds + }, _.strip_empty_properties(extra_props)); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) + }); + }, + + mpPageViewProperties: function() { + return _.strip_empty_properties({ + 'current_page_title': document$1.title, + 'current_domain': win.location.hostname, + 'current_url_path': win.location.pathname, + 'current_url_protocol': win.location.protocol, + 'current_url_search': win.location.search + }); + } +}; + +var cheap_guid = function(maxlen) { + var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); + return maxlen ? guid.substring(0, maxlen) : guid; +}; + +// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) +var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; +// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk +var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; +/** + * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For + * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we + * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). + * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy + * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel + * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date + * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK + * offers the 'cookie_domain' config option to set it explicitly. + * @example + * extract_domain('my.sub.example.com') + * // 'example.com' + */ +var extract_domain = function(hostname) { + var domain_regex = DOMAIN_MATCH_REGEX; + var parts = hostname.split('.'); + var tld = parts[parts.length - 1]; + if (tld.length > 4 || tld === 'com' || tld === 'org') { + domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; + } + var matches = hostname.match(domain_regex); + return matches ? matches[0] : ''; +}; + +var JSONStringify = null, JSONParse = null; +if (typeof JSON !== 'undefined') { + JSONStringify = JSON.stringify; + JSONParse = JSON.parse; +} +JSONStringify = JSONStringify || _.JSONEncode; +JSONParse = JSONParse || _.JSONDecode; + +// EXPORTS (for closure compiler) +_['toArray'] = _.toArray; +_['isObject'] = _.isObject; +_['JSONEncode'] = _.JSONEncode; +_['JSONDecode'] = _.JSONDecode; +_['isBlockedUA'] = _.isBlockedUA; +_['isEmptyObject'] = _.isEmptyObject; +_['info'] = _.info; +_['info']['device'] = _.info.device; +_['info']['browser'] = _.info.browser; +_['info']['browserVersion'] = _.info.browserVersion; +_['info']['properties'] = _.info.properties; + +/* eslint camelcase: "off" */ + +/** + * DomTracker Object + * @constructor + */ +var DomTracker = function() {}; + + +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; + +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; + +/** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback + */ +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; +}; + +/** + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; +}; + +DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; + +var logger$2 = console_with_prefix('lock'); + +/** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ +var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; +}; + +// pass in a specific pid to test contention scenarios; otherwise +// it is chosen randomly for each acquisition attempt +SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } +}; + +var logger$1 = console_with_prefix('batch'); + +/** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ +var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; +}; + +/** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ +RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ +RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; +}; + +/** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ +var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; +}; + +/** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ +RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); +}; + +// internal helper for RequestQueue.updatePayloads +var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; +}; + +/** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ +RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ +RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; +}; + +/** + * Serialize the given items array to localStorage. + */ +RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } +}; + +/** + * Clear out queues (memory and localStorage). + */ +RequestQueue.prototype.clear = function() { + this.memQueue = []; + this.storage.removeItem(this.storageKey); +}; + +// maximum interval between request retries after exponential backoff +var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + +var logger = console_with_prefix('batch'); + +/** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ +var RequestBatcher = function(storageKey, options) { + this.errorReporter = options.errorReporter; + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage + }); + + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; + + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; + + this.stopped = !this.libConfig['batch_autostart']; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; +}; + +/** + * Add one item to queue. + */ +RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.flushInterval, cb); +}; + +/** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ +RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); +}; + +/** + * Stop flushing batches. Can be restarted by calling start(). + */ +RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } +}; + +/** + * Clear out queue. + */ +RequestBatcher.prototype.clear = function() { + this.queue.clear(); +}; + +/** + * Restore batch size configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetBatchSize = function() { + this.batchSize = this.libConfig['batch_size']; +}; + +/** + * Restore flush interval time configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); +}; + +/** + * Schedule the next flush in the given number of milliseconds. + */ +RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.flushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + } +}; + +/** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ +RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var startTime = new Date().getTime(); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + res.xhr_req && + (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + var headers = res.xhr_req['responseHeaders']; + if (headers) { + var retryAfter = headers['Retry-After']; + if (retryAfter) { + retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; + } + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + this.flush(); // handle next batch if the queue isn't empty + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.stopAllBatching(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger.log('MIXPANEL REQUEST:', dataForRequest); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } +}; + +/** + * Log error to global logger and optional user-defined logger. + */ +RequestBatcher.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + if (this.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.errorReporter(msg, err); + } catch(err) { + logger.error(err); + } + } +}; + +/** + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. + */ + +/** + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + +/** Public **/ + +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} + +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type + */ +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} + +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} + +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} + +/** Private **/ + +/** + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage + */ +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} + +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} + +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} + +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; + + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); + + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; + } + + options = options || {}; + + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} + +/** + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; + + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } + + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +/* eslint camelcase: "off" */ + +/** @const */ var SET_ACTION = '$set'; +/** @const */ var SET_ONCE_ACTION = '$set_once'; +/** @const */ var UNSET_ACTION = '$unset'; +/** @const */ var ADD_ACTION = '$add'; +/** @const */ var APPEND_ACTION = '$append'; +/** @const */ var UNION_ACTION = '$union'; +/** @const */ var REMOVE_ACTION = '$remove'; +/** @const */ var DELETE_ACTION = '$delete'; + +// Common internal methods for mixpanel.people and mixpanel.group APIs. +// These methods shouldn't involve network I/O. +var apiActions = { + set_action: function(prop, to) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + } else { + $set[prop] = to; + } + + data[SET_ACTION] = $set; + return data; + }, + + unset_action: function(prop) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + return data; + }, + + set_once_action: function(prop, to) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return data; + }, + + union_action: function(list_name, values) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + return data; + }, + + append_action: function(list_name, value) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + return data; + }, + + remove_action: function(list_name, value) { + var data = {}; + var $remove = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $remove[k] = v; + } + }, this); + } else { + $remove[list_name] = value; + } + data[REMOVE_ACTION] = $remove; + return data; + }, + + delete_action: function() { + var data = {}; + data[DELETE_ACTION] = ''; + return data; + } +}; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel Group Object + * @constructor + */ +var MixpanelGroup = function() {}; + +_.extend(MixpanelGroup.prototype, apiActions); + +MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { + this._mixpanel = mixpanel_instance; + this._group_key = group_key; + this._group_id = group_id; +}; + +/** + * Set properties on a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Set properties on a group, only if they do not yet exist. + * This will not overwrite previous group property values, unlike + * group.set(). + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set_once({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, lists or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Unset properties on a group permanently. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').unset('Founded'); + * + * @param {String} prop The name of the property. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/** + * Merge a given list with a list-valued group property, excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); + * + * @param {String} list_name Name of the property. + * @param {Array} values Values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/** + * Permanently delete a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').delete(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { + // bracket notation above prevents a minification error related to reserved words + var data = this.delete_action(); + return this._send_request(data, callback); +}); + +/** + * Remove a property from a group. The value will be ignored if doesn't exist. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); + * + * @param {String} list_name Name of the property. + * @param {Object} value Value to remove from the given group property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +MixpanelGroup.prototype._send_request = function(data, callback) { + data['$group_key'] = this._group_key; + data['$group_id'] = this._group_id; + data['$token'] = this._get_config('token'); + + var date_encoded_data = _.encodeDates(data); + return this._mixpanel._track_or_batch({ + type: 'groups', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], + batcher: this._mixpanel.request_batchers.groups + }, callback); +}; + +MixpanelGroup.prototype._is_reserved_property = function(prop) { + return prop === '$group_key' || prop === '$group_id'; +}; + +MixpanelGroup.prototype._get_config = function(conf) { + return this._mixpanel.get_config(conf); +}; + +MixpanelGroup.prototype.toString = function() { + return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; +}; + +// MixpanelGroup Exports +MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; +MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; +MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; +MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; +MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; +MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel People Object + * @constructor + */ +var MixpanelPeople = function() {}; + +_.extend(MixpanelPeople.prototype, apiActions); + +MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; +}; + +/* +* Set properties on a user record. +* +* ### Usage: +* +* mixpanel.people.set('gender', 'm'); +* +* // or set multiple properties at once +* mixpanel.people.set({ +* 'Company': 'Acme', +* 'Plan': 'Premium', +* 'Upgrade date': new Date() +* }); +* // properties can be strings, integers, dates, or lists +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document.referrer); + } + + // update $set object with default people properties + data[SET_ACTION] = _.extend( + {}, + _.info.people_properties(), + data[SET_ACTION] + ); + return this._send_request(data, callback); +}); + +/* +* Set properties on a user record, only if they do not yet exist. +* This will not overwrite previous people property values, unlike +* people.set(). +* +* ### Usage: +* +* mixpanel.people.set_once('First Login Date', new Date()); +* +* // or set multiple properties at once +* mixpanel.people.set_once({ +* 'First Login Date': new Date(), +* 'Starting Plan': 'Premium' +* }); +* +* // properties can be strings, integers or dates +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/* +* Unset properties on a user record (permanently removes the properties and their values from a profile). +* +* ### Usage: +* +* mixpanel.people.unset('gender'); +* +* // or unset multiple properties at once +* mixpanel.people.unset(['gender', 'Company']); +* +* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/* +* Increment/decrement numeric people analytics properties. +* +* ### Usage: +* +* mixpanel.people.increment('page_views', 1); +* +* // or, for convenience, if you're just incrementing a counter by +* // 1, you can simply do +* mixpanel.people.increment('page_views'); +* +* // to decrement a counter, pass a negative number +* mixpanel.people.increment('credits_left', -1); +* +* // like mixpanel.people.set(), you can increment multiple +* // properties at once: +* mixpanel.people.increment({ +* counter1: 1, +* counter2: 6 +* }); +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. +* @param {Number} [by] An amount to increment the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); +}); + +/* +* Append a value to a list-valued people analytics property. +* +* ### Usage: +* +* // append a value to a list, creating it if needed +* mixpanel.people.append('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.append({ +* list1: 'bob', +* list2: 123 +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value An item to append to the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.append_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Remove a value from a list-valued people analytics property. +* +* ### Usage: +* +* mixpanel.people.remove('School', 'UCB'); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value Item to remove from the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Merge a given list with a list-valued people analytics property, +* excluding duplicate values. +* +* ### Usage: +* +* // merge a value to a list, creating it if needed +* mixpanel.people.union('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.union({ +* list1: 'bob', +* list2: 123 +* }); +* +* // like mixpanel.people.append(), you can append multiple +* // values to the same list: +* mixpanel.people.union({ +* list1: ['bob', 'billy'] +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] Value / values to merge with the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + * @deprecated + */ +MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); +}); + +/* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * @deprecated + */ +MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); +}; + +/* +* Permanently deletes the current people analytics profile from +* Mixpanel (using the current distinct_id). +* +* ### Usage: +* +* // remove the all data you have stored about the current user +* mixpanel.people.delete_user(); +* +*/ +MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); +}; + +MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; +}; + +MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + var device_id = this._mixpanel.get_property('$device_id'); + var user_id = this._mixpanel.get_property('$user_id'); + var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); + if (device_id) { + data['$device_id'] = device_id; + } + if (user_id) { + data['$user_id'] = user_id; + } + if (had_persisted_distinct_id) { + data['$had_persisted_distinct_id'] = had_persisted_distinct_id; + } + + var date_encoded_data = _.encodeDates(data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return _.truncate(date_encoded_data, 255); + } + + return this._mixpanel._track_or_batch({ + type: 'people', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], + batcher: this._mixpanel.request_batchers.people + }, callback); +}; + +MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); +}; + +MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; +}; + +// Queue up engage operations if identify hasn't been called yet. +MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (REMOVE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console.error('Invalid call to _enqueue():', data); + } +}; + +MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + _this._mixpanel['persistence'].save(); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } +}; + +// Flush queued engage operations - order does not matter, +// and there are network level race conditions anyway +MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + var _this = this; + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var append_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + $append_item = $append_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($append_item)) { + _this.append($append_item, append_callback); + } + } + } + + // same for $remove + var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { + var $remove_item; + var remove_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); + } + if (!_.isUndefined(_remove_callback)) { + _remove_callback(response, data); + } + }; + for (var j = $remove_queue.length - 1; j >= 0; j--) { + $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + $remove_item = $remove_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($remove_item)) { + _this.remove($remove_item, remove_callback); + } + } + } +}; + +MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; +}; + +// MixpanelPeople Exports +MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; +MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; +MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; +MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; +MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; +MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; +MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; +MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; +MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; +MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; +MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + +/* eslint camelcase: "off" */ + +/* + * Constants + */ +/** @const */ var SET_QUEUE_KEY = '__mps'; +/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; +/** @const */ var UNSET_QUEUE_KEY = '__mpus'; +/** @const */ var ADD_QUEUE_KEY = '__mpa'; +/** @const */ var APPEND_QUEUE_KEY = '__mpap'; +/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; +/** @const */ var UNION_QUEUE_KEY = '__mpu'; +// This key is deprecated, but we want to check for it to see whether aliasing is allowed. +/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; +/** @const */ var ALIAS_ID_KEY = '__alias'; +/** @const */ var EVENT_TIMERS_KEY = '__timers'; +/** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + REMOVE_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + EVENT_TIMERS_KEY +]; + +/** + * Mixpanel Persistence Object + * @constructor + */ +var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + if (storage_type === 'localStorage' && _.localStorage.is_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(); + this.save(); +}; + +MixpanelPersistence.prototype.properties = function() { + var p = {}; + + this.load(); + + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; +}; + +MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } +}; + +MixpanelPersistence.prototype.upgrade = function() { + var old_cookie, + old_localstorage; + + // if transferring from cookie to localStorage or vice-versa, copy existing + // super properties over to new storage mode + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } else if (this.storage === _.cookie) { + old_localstorage = _.localStorage.parse(this.name); + + _.localStorage.remove(this.name); + + if (old_localstorage) { + this.register_once(old_localstorage); + } + } +}; + +MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure, + this.cross_site, + this.cookie_domain + ); +}; + +MixpanelPersistence.prototype.load_prop = function(key) { + this.load(); + return this['props'][key]; +}; + +MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false, this.cookie_domain); + this.storage.remove(this.name, true, this.cookie_domain); +}; + +// removes the storage entry and deletes all loaded data +// forced name for tests +MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; +}; + +/** +* @param {Object} props +* @param {*=} default_value +* @param {number=} days +*/ +MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; +}; + +/** +* @param {Object} props +* @param {number=} days +*/ +MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + _.extend(this['props'], props); + this.save(); + + return true; + } + return false; +}; + +MixpanelPersistence.prototype.unregister = function(prop) { + this.load(); + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } +}; + +MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); +}; + +// EXPORTED METHOD, we test this directly. +MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); +}; + +MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); +}; + +MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cookie_domain(config['cookie_domain']); + this.set_cross_site(config['cross_site_cookie']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); +}; + +MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } else { + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { + if (cookie_domain !== this.cookie_domain) { + this.remove(); + this.cookie_domain = cookie_domain; + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_site = function(cross_site) { + if (cross_site !== this.cross_site) { + this.cross_site = cross_site; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; +}; + +MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + remove_q = this._get_or_create_queue(REMOVE_ACTION, []), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === REMOVE_QUEUE_KEY) { + remove_q.push(q_data); + this._pop_from_people_queue(APPEND_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console.log(data); + + this.save(); +}; + +MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this['props'][this._get_queue_key(queue)]; + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { + // list actions: only remove if both k+v match + // e.g. remove should not override append in a case like + // append({foo: 'bar'}); remove({foo: 'qux'}) + _.each(q, function(queued_action) { + if (queued_action[k] === v) { + delete queued_action[k]; + } + }); + } else { + delete q[k]; + } + }, this); + } +}; + +MixpanelPersistence.prototype.load_queue = function(queue) { + return this.load_prop(this._get_queue_key(queue)); +}; + +MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === REMOVE_ACTION) { + return REMOVE_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console.error('Invalid queue:', queue); + } +}; + +MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + return this['props'][key] || (this['props'][key] = default_val); +}; + +MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); +}; + +MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; +}; + +/* eslint camelcase: "off" */ + +/* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @output_file_name mixpanel-2.8.min.js +// ==/ClosureCompiler== + +/* +SIMPLE STYLE GUIDE: + +this.x === public function +this._x === internal - only use within this file +this.__x === private - only use within the class + +Globals should be all caps +*/ + +var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + +var mixpanel_master; // main mixpanel instance / object +var INIT_MODULE = 0; +var INIT_SNIPPET = 1; + +var IDENTITY_FUNC = function(x) {return x;}; +var NOOP_FUNC = function() {}; + +/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; +/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; +/** @const */ var PAYLOAD_TYPE_JSON = 'json'; +/** @const */ var DEVICE_ID_PREFIX = '$device:'; + + +/* + * Dynamic... constants? Is that an oxymoron? + */ +// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ +// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials +var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + +// IE<10 does not support cross-origin XHR's but script tags +// with defer won't block window.onload; ENQUEUE_REQUESTS +// should only be true for Opera<12 +var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + +// save reference to navigator.sendBeacon so it can be minified +var sendBeacon = null; +if (navigator['sendBeacon']) { + sendBeacon = function() { + // late reference to navigator.sendBeacon to allow patching/spying + return navigator['sendBeacon'].apply(navigator, arguments); + }; +} + +var DEFAULT_API_ROUTES = { + 'track': 'track/', + 'engage': 'engage/', + 'groups': 'groups/', + 'record': 'record/' +}; + +/* + * Module-level globals + */ +var DEFAULT_CONFIG = { + 'api_host': 'https://api-js.mixpanel.com', + 'api_routes': DEFAULT_API_ROUTES, + 'api_method': 'POST', + 'api_transport': 'XHR', + 'api_payload_format': PAYLOAD_TYPE_BASE64, + 'app_host': 'https://mixpanel.com', + 'cdn': 'https://cdn.mxpnl.com', + 'cross_site_cookie': false, + 'cross_subdomain_cookie': true, + 'error_reporter': NOOP_FUNC, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_domain': '', + 'cookie_name': '', + 'loaded': NOOP_FUNC, + 'mp_loader': null, + 'track_marketing': true, + 'track_pageview': false, + 'skip_first_touch_marketing': false, + 'store_google': true, + 'stop_utm_persistence': false, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'opt_out_tracking_by_default': false, + 'opt_out_persistence_by_default': false, + 'opt_out_tracking_persistence_type': 'localStorage', + 'opt_out_tracking_cookie_prefix': null, + 'property_blacklist': [], + 'xhr_headers': {}, // { header: value, header2: value } + 'ignore_dnt': false, + 'batch_requests': true, + 'batch_size': 50, + 'batch_flush_interval_ms': 5000, + 'batch_request_timeout_ms': 90000, + 'batch_autostart': true, + 'hooks': {}, + 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), + 'record_block_selector': 'img, video', + 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), + 'record_mask_text_selector': '*', + 'record_max_ms': MAX_RECORDING_MS, + 'record_sessions_percent': 0, + 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' +}; + +var DOM_LOADED = false; + +/** + * Mixpanel Library Object + * @constructor + */ +var MixpanelLib = function() {}; + + +/** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ +var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._cached_groups = {}; // cache groups in a pool + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + if (!instance.get_config('skip_first_touch_marketing')) { + // We need null UTM params in the object because + // UTM parameters act as a tuple. If any UTM param + // is present, then we set all UTM params including + // empty ones together + var utm_params = _.info.campaignParams(null); + var initial_utm_params = {}; + var has_utm = false; + _.each(utm_params, function(utm_value, utm_key) { + initial_utm_params['initial_' + utm_key] = utm_value; + if (utm_value) { + has_utm = true; + } + }); + if (has_utm) { + instance['people'].set_once(initial_utm_params); + } + } + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; +}; + +// Initialization methods + +/** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ +MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + this.report_error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; +}; + +// mixpanel._init(token:string, config:object, name:string) +// +// This function sets up the current instance of the mixpanel +// library. The difference between this method and the init(...) +// method is this one initializes the actual instance, whereas the +// init(...) method sets up a new library and calls _init on it. +// +MixpanelLib.prototype._init = function(token, config, name) { + config = config || {}; + + this['__loaded'] = true; + this['config'] = {}; + + var variable_features = {}; + + // default to JSON payload for standard mixpanel.com API hosts + if (!('api_payload_format' in config)) { + var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; + if (api_host.match(/\.mixpanel\.com/)) { + variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; + } + } + + this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = NOOP_FUNC; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + // set up request queueing/batching + this.request_batchers = {}; + this._batch_requests = this.get_config('batch_requests'); + if (this._batch_requests) { + if (!_.localStorage.is_supported(true) || !USE_XHR) { + this._batch_requests = false; + console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + _.each(this.get_batcher_configs(), function(batcher_config) { + console.log('Clearing batch queue ' + batcher_config.queue_key); + _.localStorage.remove(batcher_config.queue_key); + }); + } else { + this.init_batchers(); + if (sendBeacon && win.addEventListener) { + // Before page closes or hides (user tabs away etc), attempt to flush any events + // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, + // events will not be removed from the persistent store; if the site is loaded again, + // the events will be flushed again on startup and deduplicated on the Mixpanel server + // side. + // There is no reliable way to capture only page close events, so we lean on the + // visibilitychange and pagehide events as recommended at + // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. + // These events fire when the user clicks away from the current page/tab, so will occur + // more frequently than page unload, but are the only mechanism currently for capturing + // this scenario somewhat reliably. + var flush_on_unload = _.bind(function() { + if (!this.request_batchers.events.stopped) { + this.request_batchers.events.flush({unloading: true}); + } + }, this); + win.addEventListener('pagehide', function(ev) { + if (ev['persisted']) { + flush_on_unload(); + } + }); + win.addEventListener('visibilitychange', function() { + if (document$1['visibilityState'] === 'hidden') { + flush_on_unload(); + } + }); + } + } + } + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.unpersisted_superprops = {}; + this._gdpr_init(); + + var uuid = _.UUID(); + if (!this.get_distinct_id()) { + // There is no need to set the distinct id + // or the device id if something was already stored + // in the persitence + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); + } + + var track_pageview_option = this.get_config('track_pageview'); + if (track_pageview_option) { + this._init_url_change_tracking(track_pageview_option); + } + + if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { + this.start_session_recording(); + } +}; + +MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { + if (!win['MutationObserver']) { + console.critical('Browser does not support MutationObserver; skipping session recording'); + return; + } + + var handleLoadedRecorder = _.bind(function() { + this._recorder = this._recorder || new win['__mp_recorder'](this); + this._recorder['startRecording'](); + }, this); + + if (_.isUndefined(win['__mp_recorder'])) { + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); + } else { + handleLoadedRecorder(); + } +}); + +MixpanelLib.prototype.stop_session_recording = function () { + if (this._recorder) { + this._recorder['stopRecording'](); + } else { + console.critical('Session recorder module not loaded'); + } +}; + +MixpanelLib.prototype.get_session_recording_properties = function () { + var props = {}; + if (this._recorder) { + var replay_id = this._recorder['replayId']; + if (replay_id) { + props['$mp_replay_id'] = replay_id; + } + } + return props; +}; + +// Private methods + +MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + this._set_default_superprops(); + this['people'].set_once(this['persistence'].get_referrer_info()); + + // `store_google` is now deprecated and previously stored UTM parameters are cleared + // from persistence by default. + if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { + var utm_params = _.info.campaignParams(null); + _.each(utm_params, function(_utm_value, utm_key) { + // We need to unregister persisted UTM parameters so old values + // are not mixed with the new UTM parameters + this.unregister(utm_key); + }.bind(this)); + } +}; + +// update persistence with info on referrer, UTM params, etc +MixpanelLib.prototype._set_default_superprops = function() { + this['persistence'].update_search_keyword(document$1.referrer); + // Registering super properties for UTM persistence by 'store_google' is deprecated. + if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { + this.register(_.info.campaignParams()); + } + if (this.get_config('save_referrer')) { + this['persistence'].update_referrer_info(document$1.referrer); + } +}; + +MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + + if (!this.has_opted_out_tracking()) { + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + } + + delete this.__dom_loaded_queue; + delete this.__request_queue; +}; + +MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + this.report_error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); +}; + +MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { + var previous_tracked_url = ''; + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = _.info.currentUrl(); + } + + if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { + win.addEventListener('popstate', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + win.addEventListener('hashchange', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + var nativePushState = win.history.pushState; + if (typeof nativePushState === 'function') { + win.history.pushState = function(state, unused, url) { + nativePushState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + var nativeReplaceState = win.history.replaceState; + if (typeof nativeReplaceState === 'function') { + win.history.replaceState = function(state, unused, url) { + nativeReplaceState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + win.addEventListener('mp_locationchange', function() { + var current_url = _.info.currentUrl(); + var should_track = false; + if (track_pageview_option === 'full-url') { + should_track = current_url !== previous_tracked_url; + } else if (track_pageview_option === 'url-with-path-and-query-string') { + should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; + } else if (track_pageview_option === 'url-with-path') { + should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; + } + + if (should_track) { + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = current_url; + } + } + }.bind(this)); + } +}; + +/** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ +MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } +}; + +MixpanelLib.prototype._send_request = function(url, data, options, callback) { + var succeeded = true; + + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return succeeded; + } + + var DEFAULT_OPTIONS = { + method: this.get_config('api_method'), + transport: this.get_config('api_transport'), + verbose: this.get_config('verbose') + }; + var body_data = null; + + if (!callback && (_.isFunction(options) || typeof options === 'string')) { + callback = options; + options = null; + } + options = _.extend(DEFAULT_OPTIONS, options || {}); + if (!USE_XHR) { + options.method = 'GET'; + } + var use_post = options.method === 'POST'; + var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; + + // needed to correctly format responses + var verbose_mode = options.verbose; + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + + if (use_post) { + body_data = 'data=' + encodeURIComponent(data['data']); + delete data['data']; + } + + url += '?' + _.HTTPBuildQuery(data); + + var lib = this; + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (use_sendBeacon) { + try { + succeeded = sendBeacon(url, body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + try { + if (callback) { + callback(succeeded ? 1 : 0); + } + } catch (e) { + lib.report_error(e); + } + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + + var headers = this.get_config('xhr_headers'); + if (use_post) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + + return succeeded; +}; + +/** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ +MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (_.isArray(fn_name)) { + tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() + } else if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + if (_.isArray(item[0])) { + // chained call + var caller = context; + _.each(item, function(call) { + caller = caller[call[0]].apply(caller, call.slice(1)); + }); + } else { + this[item[0]].apply(this, item.slice(1)); + } + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); +}; + +// request queueing utils + +MixpanelLib.prototype.are_batchers_initialized = function() { + return !!this.request_batchers.events; +}; + +MixpanelLib.prototype.get_batcher_configs = function() { + var queue_prefix = '__mpq_' + this.get_config('token'); + var api_routes = this.get_config('api_routes'); + this._batcher_configs = this._batcher_configs || { + events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, + people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, + groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} + }; + return this._batcher_configs; +}; + +MixpanelLib.prototype.init_batchers = function() { + if (!this.are_batchers_initialized()) { + var batcher_for = _.bind(function(attrs) { + return new RequestBatcher( + attrs.queue_key, + { + libConfig: this['config'], + sendRequestFunc: _.bind(function(data, options, cb) { + this._send_request( + this.get_config('api_host') + attrs.endpoint, + this._encode_data_for_request(data), + options, + this._prepare_callback(cb, data) + ); + }, this), + beforeSendHook: _.bind(function(item) { + return this._run_hook('before_send_' + attrs.type, item); + }, this), + errorReporter: this.get_config('error_reporter'), + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + } + ); + }, this); + var batcher_configs = this.get_batcher_configs(); + this.request_batchers = { + events: batcher_for(batcher_configs.events), + people: batcher_for(batcher_configs.people), + groups: batcher_for(batcher_configs.groups) + }; + } + if (this.get_config('batch_autostart')) { + this.start_batch_senders(); + } +}; + +MixpanelLib.prototype.start_batch_senders = function() { + this._batchers_were_started = true; + if (this.are_batchers_initialized()) { + this._batch_requests = true; + _.each(this.request_batchers, function(batcher) { + batcher.start(); + }); + } +}; + +MixpanelLib.prototype.stop_batch_senders = function() { + this._batch_requests = false; + _.each(this.request_batchers, function(batcher) { + batcher.stop(); + batcher.clear(); + }); +}; + +/** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ +MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); +}; + +/** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ +MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } +}; + +MixpanelLib.prototype._encode_data_for_request = function(data) { + var encoded_data = _.JSONEncode(data); + if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { + encoded_data = _.base64Encode(encoded_data); + } + return {'data': encoded_data}; +}; + +// internal method for handling track vs batch-enqueue logic +MixpanelLib.prototype._track_or_batch = function(options, callback) { + var truncated_data = _.truncate(options.data, 255); + var endpoint = options.endpoint; + var batcher = options.batcher; + var should_send_immediately = options.should_send_immediately; + var send_request_options = options.send_request_options || {}; + callback = callback || NOOP_FUNC; + + var request_enqueued_or_initiated = true; + var send_request_immediately = _.bind(function() { + if (!send_request_options.skip_hooks) { + truncated_data = this._run_hook('before_send_' + options.type, truncated_data); + } + if (truncated_data) { + console.log('MIXPANEL REQUEST:'); + console.log(truncated_data); + return this._send_request( + endpoint, + this._encode_data_for_request(truncated_data), + send_request_options, + this._prepare_callback(callback, truncated_data) + ); + } else { + return null; + } + }, this); + + if (this._batch_requests && !should_send_immediately) { + batcher.enqueue(truncated_data, function(succeeded) { + if (succeeded) { + callback(1, truncated_data); + } else { + send_request_immediately(); + } + }); + } else { + request_enqueued_or_initiated = send_request_immediately(); + } + + return request_enqueued_or_initiated && truncated_data; +}; + +/** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // track an event using navigator.sendBeacon + * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this track request. + * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). + * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + var transport = options['transport']; // external API, don't minify 'transport' prop + if (transport) { + options.transport = transport; // 'transport' prop name can be minified internally + } + var should_send_immediately = options['send_immediately']; + if (typeof callback !== 'function') { + callback = NOOP_FUNC; + } + + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = _.extend({}, properties); + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + this._set_default_superprops(); + + var marketing_properties = this.get_config('track_marketing') + ? _.info.marketingParams() + : {}; + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties({'mp_loader': this.get_config('mp_loader')}), + marketing_properties, + this['persistence'].properties(), + this.unpersisted_superprops, + this.get_session_recording_properties(), + properties + ); + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + var ret = this._track_or_batch({ + type: 'events', + data: data, + endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], + batcher: this.request_batchers.events, + should_send_immediately: should_send_immediately, + send_request_options: options + }, callback); + + return ret; +}); + +/** + * Register the current user into one/many groups. + * + * ### Usage: + * + * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs + * mixpanel.set_group('company', 'mixpanel') + * mixpanel.set_group('company', 128746312) + * + * @param {String} group_key Group key + * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * + */ +MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { + if (!_.isArray(group_ids)) { + group_ids = [group_ids]; + } + var prop = {}; + prop[group_key] = group_ids; + this.register(prop); + return this['people'].set(group_key, group_ids, callback); +}); + +/** + * Add a new group for this user. + * + * ### Usage: + * + * mixpanel.add_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_values = this.get_property(group_key); + var prop = {}; + if (old_values === undefined) { + prop[group_key] = [group_id]; + this.register(prop); + } else { + if (old_values.indexOf(group_id) === -1) { + old_values.push(group_id); + prop[group_key] = old_values; + this.register(prop); + } + } + return this['people'].union(group_key, group_id, callback); +}); + +/** + * Remove a group from this user. + * + * ### Usage: + * + * mixpanel.remove_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_value = this.get_property(group_key); + // if the value doesn't exist, the persistent store is unchanged + if (old_value !== undefined) { + var idx = old_value.indexOf(group_id); + if (idx > -1) { + old_value.splice(idx, 1); + this.register({group_key: old_value}); + } + if (old_value.length === 0) { + this.unregister(group_key); + } + } + return this['people'].remove(group_key, group_id, callback); +}); + +/** + * Track an event with specific groups. + * + * ### Usage: + * + * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) + * + * @param {String} event_name The name of the event (see `mixpanel.track()`) + * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) + * @param {Object=} groups An object mapping group name keys to one or more values + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { + var tracking_props = _.extend({}, properties || {}); + _.each(groups, function(v, k) { + if (v !== null && v !== undefined) { + tracking_props[k] = v; + } + }); + return this.track(event_name, tracking_props, callback); +}); + +MixpanelLib.prototype._create_map_key = function (group_key, group_id) { + return group_key + '_' + JSON.stringify(group_id); +}; + +MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { + delete this._cached_groups[this._create_map_key(group_key, group_id)]; +}; + +/** + * Look up reference to a Mixpanel group + * + * ### Usage: + * + * mixpanel.get_group(group_key, group_id) + * + * @param {String} group_key Group key + * @param {Object} group_id A valid Mixpanel property type + * @returns {Object} A MixpanelGroup identifier + */ +MixpanelLib.prototype.get_group = function (group_key, group_id) { + var map_key = this._create_map_key(group_key, group_id); + var group = this._cached_groups[map_key]; + if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { + group = new MixpanelGroup(); + group._init(this, group_key, group_id); + this._cached_groups[map_key] = group; + } + return group; +}; + +/** + * Track a default Mixpanel page view event, which includes extra default event properties to + * improve page view data. + * + * ### Usage: + * + * // track a default $mp_web_page_view event + * mixpanel.track_pageview(); + * + * // track a page view event with additional event properties + * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); + * + * // example approach to track page views on different page types as event properties + * mixpanel.track_pageview({'page': 'pricing'}); + * mixpanel.track_pageview({'page': 'homepage'}); + * + * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for + * // individual pages on the same site or product. Use cases for custom event_name may be page + * // views on different products or internal applications that are considered completely separate + * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); + * + * ### Notes: + * + * The `config.track_pageview` option for mixpanel.init() + * may be turned on for tracking page loads automatically. + * + * // track only page loads + * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); + * + * // track when the URL changes in any manner + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); + * + * // track when the URL changes, ignoring any changes in the hash part + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); + * + * // track when the path changes, ignoring any query parameter or hash changes + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); + * + * @param {Object} [properties] An optional set of additional properties to send with the page view event + * @param {Object} [options] Page view tracking options + * @param {String} [options.event_name] - Alternate name for the tracking event + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { + if (typeof properties !== 'object') { + properties = {}; + } + options = options || {}; + var event_name = options['event_name'] || '$mp_web_page_view'; + + var default_page_properties = _.extend( + _.info.mpPageViewProperties(), + _.info.campaignParams(), + _.info.clickParams() + ); + + var event_properties = _.extend( + {}, + default_page_properties, + properties + ); + + return this.track(event_name, event_properties); +}); + +/** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ +MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); +}; + +/** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ +MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); +}; + +/** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ +MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); +}; + +var REGISTER_DEFAULTS = { + 'persistent': true +}; +/** + * Helper to parse options param for register methods, maintaining + * legacy support for plain "days" param instead of options object + * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods + * @returns {Object} options object + */ +var options_for_register = function(days_or_options) { + var options; + if (_.isObject(days_or_options)) { + options = days_or_options; + } else if (!_.isUndefined(days_or_options)) { + options = {'days': days_or_options}; + } else { + options = {}; + } + return _.extend({}, REGISTER_DEFAULTS, options); +}; + +/** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * // register only for the current pageload + * mixpanel.register({'Name': 'Pat'}, {persistent: false}); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register = function(props, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register(props, options['days']); + } else { + _.extend(this.unpersisted_superprops, props); + } +}; + +/** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * // register once, only for the current pageload + * mixpanel.register_once({ + * 'First interaction time': new Date().toISOString() + * }, 'None', {persistent: false}); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register_once(props, default_value, options['days']); + } else { + if (typeof(default_value) === 'undefined') { + default_value = 'None'; + } + _.each(props, function(val, prop) { + if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { + this.unpersisted_superprops[prop] = val; + } + }, this); + } +}; + +/** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + * @param {Object} [options] + * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.unregister = function(property, options) { + options = options_for_register(options); + if (options['persistent']) { + this['persistence'].unregister(property); + } else { + delete this.unpersisted_superprops[property]; + } +}; + +MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); +}; + +/** + * Identify a user with a unique ID to track user activity across + * devices, tie a user to their events, and create a user profile. + * If you never call this method, unique visitors are tracked using + * a UUID generated the first time they visit the site. + * + * Call identify when you know the identity of the current user, + * typically after login or signup. We recommend against using + * identify for anonymous visitors to your site. + * + * ### Notes: + * If your project has + * ID Merge + * enabled, the identify method will connect pre- and + * post-authentication events when appropriate. + * + * If your project does not have ID Merge enabled, identify will + * change the user's local distinct_id to the unique ID you pass. + * Events tracked prior to authentication will not be connected + * to the same user identity. If ID Merge is disabled, alias can + * be used to connect pre- and post-registration events. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ +MixpanelLib.prototype.identify = function( + new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + var previous_distinct_id = this.get_distinct_id(); + if (new_distinct_id && previous_distinct_id !== new_distinct_id) { + // we allow the following condition if previous distinct_id is same as new_distinct_id + // so that you can force flush people updates for anonymous profiles. + if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { + this.report_error('distinct_id cannot have $device: prefix'); + return -1; + } + this.register({'$user_id': new_distinct_id}); + } + + if (!this.get_property('$device_id')) { + // The persisted distinct id might not actually be a device id at all + // it might be a distinct id of the user from before + var device_id = previous_distinct_id; + this.register_once({ + '$had_persisted_distinct_id': true, + '$device_id': device_id + }, ''); + } + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this.register({'distinct_id': new_distinct_id}); + } + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); + + // send an $identify event any time the distinct_id is changing - logic on the server + // will determine whether or not to do anything with it. + if (new_distinct_id !== previous_distinct_id) { + this.track('$identify', { + 'distinct_id': new_distinct_id, + '$anon_distinct_id': previous_distinct_id + }, {skip_hooks: true}); + } +}; + +/** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ +MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + var uuid = _.UUID(); + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); +}; + +/** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ +MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); +}; + +/** + * The alias method creates an alias which Mixpanel will use to + * remap one id to another. Multiple aliases can point to the + * same identifier. + * + * The following is a valid use of alias: + * + * mixpanel.alias('new_id', 'existing_id'); + * // You can add multiple id aliases to the existing ID + * mixpanel.alias('newer_id', 'existing_id'); + * + * Aliases can also be chained - the following is a valid example: + * + * mixpanel.alias('new_id', 'existing_id'); + * // chain newer_id - new_id - existing_id + * mixpanel.alias('newer_id', 'new_id'); + * + * Aliases cannot point to multiple identifiers - the following + * example will not work: + * + * mixpanel.alias('new_id', 'existing_id'); + * // this is invalid as 'new_id' already points to 'existing_id' + * mixpanel.alias('new_id', 'newer_id'); + * + * ### Notes: + * + * If your project does not have + * ID Merge + * enabled, the best practice is to call alias once when a unique + * ID is first created for a user (e.g., when a user first registers + * for an account). Do not use alias multiple times for a single + * user without ID Merge enabled. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ +MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + this.report_error('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { + 'alias': alias, + 'distinct_id': original + }, { + skip_hooks: true + }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + this.report_error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } +}; + +/** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @deprecated + */ +MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); +}; + +/** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // host for requests (customizable for e.g. a local proxy) + * api_host: 'https://api-js.mixpanel.com', + * + * // endpoints for different types of requests + * api_routes: { + * track: 'track/', + * engage: 'engage/', + * groups: 'groups/', + * } + * + * // HTTP method for tracking requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. Mixpanel + * // tracking via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // request-batching/queueing/retry + * batch_requests: true, + * + * // maximum number of events/updates to send in a single + * // network request + * batch_size: 50, + * + * // milliseconds to wait between sending batch requests + * batch_flush_interval_ms: 5000, + * + * // milliseconds to wait for network responses to batch requests + * // before they are considered timed-out and retried + * batch_request_timeout_ms: 90000, + * + * // override value for cookie domain, only useful for ensuring + * // correct cross-subdomain cookies on unusual domains like + * // subdomain.mainsite.avocat.fr; NB this cannot be used to + * // set cookies on a different domain than the current origin + * cookie_domain: '' + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // if true, cookie will be set with SameSite=None; Secure + * // this is only useful in special situations, like embedded + * // 3rd-party iframes that set up a Mixpanel instance + * cross_site_cookie: false + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of tracking by this Mixpanel instance by default + * opt_out_tracking_by_default: false + * + * // opt users out of browser data storage by this Mixpanel instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_tracking_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_tracking_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // disables enriching user profiles with first touch marketing data + * skip_first_touch_marketing: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // adds any UTM parameters and click IDs present on the page to any events fired + * track_marketing: true + * + * // enables automatic page view tracking using default page view events through + * // the track_pageview() method + * track_pageview: false + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // whether to ignore or respect the web browser's Do Not Track setting + * ignore_dnt: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ +MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + var new_batch_size = config['batch_size']; + if (new_batch_size) { + _.each(this.request_batchers, function(batcher) { + batcher.resetBatchSize(); + }); + } + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } +}; + +/** + * returns the current config object for the library. + */ +MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; +}; + +/** + * Fetch a hook function from config, with safe default, and run it + * against the given arguments + * @param {string} hook_name which hook to retrieve + * @returns {any|null} return value of user-provided hook, or null if nothing was returned + */ +MixpanelLib.prototype._run_hook = function(hook_name) { + var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); + if (typeof ret === 'undefined') { + this.report_error(hook_name + ' hook did not return a value'); + ret = null; + } + return ret; +}; + +/** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ +MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence'].load_prop([property_name]); +}; + +MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; +}; + +MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); +}; + +// perform some housekeeping around GDPR opt-in/out state +MixpanelLib.prototype._gdpr_init = function() { + var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; + + // try to convert opt-in/out cookies to localStorage if possible + if (is_localStorage_requested && _.localStorage.is_supported()) { + if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { + this.opt_in_tracking({'enable_persistence': false}); + } + if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { + this.opt_out_tracking({'clear_persistence': false}); + } + this.clear_opt_in_out_tracking({ + 'persistence_type': 'cookie', + 'enable_persistence': false + }); + } + + // check whether the user has already opted out - if so, clear & disable persistence + if (this.has_opted_out_tracking()) { + this._gdpr_update_persistence({'clear_persistence': true}); + + // check whether we should opt out by default + // note: we don't clear persistence here by default since opt-out default state is often + // used as an initial state while GDPR information is being collected + } else if (!this.has_opted_in_tracking() && ( + this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') + )) { + _.cookie.remove('mp_optout'); + this.opt_out_tracking({ + 'clear_persistence': this.get_config('opt_out_persistence_by_default') + }); + } +}; + +/** + * Enable or disable persistence based on options + * only enable/disable if persistence is not already in this state + * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it + * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence + */ +MixpanelLib.prototype._gdpr_update_persistence = function(options) { + var disabled; + if (options && options['clear_persistence']) { + disabled = true; + } else if (options && options['enable_persistence']) { + disabled = false; + } else { + return; + } + + if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { + this['persistence'].set_disabled(disabled); + } + + if (disabled) { + this.stop_batch_senders(); + } else { + // only start batchers after opt-in if they have previously been started + // in order to avoid unintentionally starting up batching for the first time + if (this._batchers_were_started) { + this.start_batch_senders(); + } + } +}; + +// call a base gdpr function after constructing the appropriate token and options args +MixpanelLib.prototype._gdpr_call_func = function(func, options) { + options = _.extend({ + 'track': _.bind(this.track, this), + 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), + 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), + 'cookie_expiration': this.get_config('cookie_expiration'), + 'cross_site_cookie': this.get_config('cross_site_cookie'), + 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), + 'cookie_domain': this.get_config('cookie_domain'), + 'secure_cookie': this.get_config('secure_cookie'), + 'ignore_dnt': this.get_config('ignore_dnt') + }, options); + + // check if localStorage can be used for recording opt out status, fall back to cookie if not + if (!_.localStorage.is_supported()) { + options['persistence_type'] = 'cookie'; + } + + return func(this.get_config('token'), { + track: options['track'], + trackEventName: options['track_event_name'], + trackProperties: options['track_properties'], + persistenceType: options['persistence_type'], + persistencePrefix: options['cookie_prefix'], + cookieDomain: options['cookie_domain'], + cookieExpiration: options['cookie_expiration'], + crossSiteCookie: options['cross_site_cookie'], + crossSubdomainCookie: options['cross_subdomain_cookie'], + secureCookie: options['secure_cookie'], + ignoreDnt: options['ignore_dnt'] + }); +}; + +/** + * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user in + * mixpanel.opt_in_tracking(); + * + * // opt user in with specific event name, properties, cookie configuration + * mixpanel.opt_in_tracking({ + * track_event_name: 'User opted in', + * track_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) + * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action + * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_in_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(optIn, options); + this._gdpr_update_persistence(options); +}; + +/** + * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user out + * mixpanel.opt_out_tracking(); + * + * // opt user out with different cookie configuration from Mixpanel instance + * mixpanel.opt_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_out_tracking = function(options) { + options = _.extend({ + 'clear_persistence': true, + 'delete_user': true + }, options); + + // delete user and clear charges since these methods may be disabled by opt-out + if (options['delete_user'] && this['people'] && this['people']._identify_called()) { + this['people'].delete_user(); + this['people'].clear_charges(); + } + + this._gdpr_call_func(optOut, options); + this._gdpr_update_persistence(options); +}; + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_in = mixpanel.has_opted_in_tracking(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ +MixpanelLib.prototype.has_opted_in_tracking = function(options) { + return this._gdpr_call_func(hasOptedIn, options); +}; + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_out = mixpanel.has_opted_out_tracking(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ +MixpanelLib.prototype.has_opted_out_tracking = function(options) { + return this._gdpr_call_func(hasOptedOut, options); +}; + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // clear user's opt-in/out status + * mixpanel.clear_opt_in_out_tracking(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_tracking/opt_out_tracking methods were called. + * mixpanel.clear_opt_in_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(clearOptInOut, options); + this._gdpr_update_persistence(options); +}; + +MixpanelLib.prototype.report_error = function(msg, err) { + console.error.apply(console.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + console.error(err); + } +}; + +// EXPORTS (for closure compiler) + +// MixpanelLib Exports +MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; +MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; +MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; +MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; +MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; +MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; +MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; +MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; +MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; +MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; +MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; +MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; +MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; +MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; +MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; +MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; +MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; +MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; +MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; +MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; +MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; +MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; +MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; +MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; +MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; +MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; +MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; +MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; +MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; +MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; +MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; +MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; +MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; +MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; +MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; + +// MixpanelPersistence Exports +MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; +MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; +MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; +MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; +MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + +var instances = {}; +var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; +}; + +var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; +}; + +var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); +}; + +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; + init_type = INIT_MODULE; + mixpanel_master = new MixpanelLib(); + + override_mp_init_func(); + mixpanel_master['init'](); + add_dom_loaded_handler(); + + return mixpanel_master; +} + +// For loading separate bundles asynchronously via script tag + +// For builds that do NOT want any extra bundles (e.g. session recorder) +// and just the main SDK, throw an error when trying to load a separate bundle. +// eslint-disable-next-line no-unused-vars +function loadThrowError (src, _onload) { + throw new Error('This build of Mixpanel only includes core SDK functionality, could not load ' + src); +} + +/* eslint camelcase: "off" */ + +var mixpanel = init_as_module(loadThrowError); + +module.exports = mixpanel; diff --git a/dist/mixpanel-with-async-recorder.cjs.js b/dist/mixpanel-with-async-recorder.cjs.js new file mode 100644 index 00000000..ba98f333 --- /dev/null +++ b/dist/mixpanel-with-async-recorder.cjs.js @@ -0,0 +1,6332 @@ +'use strict'; + +var Config = { + DEBUG: false, + LIB_VERSION: '2.53.0' +}; + +/* eslint camelcase: "off", eqeqeq: "off" */ + +// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file +var win; +if (typeof(window) === 'undefined') { + var loc = { + hostname: '' + }; + win = { + navigator: { userAgent: '' }, + document: { + location: loc, + referrer: '' + }, + screen: { width: 0, height: 0 }, + location: loc + }; +} else { + win = window; +} + +// Maximum allowed session recording length +var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours + +/* + * Saved references to long variable names, so that closure compiler can + * minimize file size. + */ + +var ArrayProto = Array.prototype, + FuncProto = Function.prototype, + ObjProto = Object.prototype, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty, + windowConsole = win.console, + navigator = win.navigator, + document$1 = win.document, + windowOpera = win.opera, + screen = win.screen, + userAgent = navigator.userAgent; + +var nativeBind = FuncProto.bind, + nativeForEach = ArrayProto.forEach, + nativeIndexOf = ArrayProto.indexOf, + nativeMap = ArrayProto.map, + nativeIsArray = Array.isArray, + breaker = {}; + +var _ = { + trim: function(str) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } +}; + +// Console override +var console = { + /** @type {function(...*)} */ + log: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } + }, + /** @type {function(...*)} */ + warn: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); + try { + windowConsole.warn.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.warn(arg); + }); + } + } + }, + /** @type {function(...*)} */ + error: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + }, + /** @type {function(...*)} */ + critical: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + } +}; + +var log_func_with_prefix = function(func, prefix) { + return function() { + arguments[0] = '[' + prefix + '] ' + arguments[0]; + return func.apply(console, arguments); + }; +}; +var console_with_prefix = function(prefix) { + return { + log: log_func_with_prefix(console.log, prefix), + error: log_func_with_prefix(console.error, prefix), + critical: log_func_with_prefix(console.critical, prefix) + }; +}; + + +// UNDERSCORE +// Embed part of the Underscore Library +_.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + if (!_.isFunction(func)) { + throw new TypeError(); + } + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) { + return func.apply(context, args.concat(slice.call(arguments))); + } + var ctor = {}; + ctor.prototype = func.prototype; + var self = new ctor(); + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; +}; + +/** + * @param {*=} obj + * @param {function(...*)=} iterator + * @param {Object=} context + */ +_.each = function(obj, iterator, context) { + if (obj === null || obj === undefined) { + return; + } + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { + return; + } + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) { + return; + } + } + } + } +}; + +_.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; +}; + +_.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; +}; + +// from a comment on http://dbj.org/dbj/?p=286 +// fails on only one very rare and deliberate custom object: +// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; +_.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } +}; + +_.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); +}; + +_.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); +}; + +_.map = function(arr, callback, context) { + if (nativeMap && arr.map === nativeMap) { + return arr.map(callback, context); + } else { + var results = []; + _.each(arr, function(item) { + results.push(callback.call(context, item)); + }); + return results; + } +}; + +_.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; +}; + +_.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; +}; + +_.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; +}; + +_.includes = function(str, needle) { + return str.indexOf(needle) !== -1; +}; + +// Underscore Addons +_.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; +}; + +_.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); +}; + +_.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; +}; + +_.isUndefined = function(obj) { + return obj === void 0; +}; + +_.isString = function(obj) { + return toString.call(obj) == '[object String]'; +}; + +_.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; + +_.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; +}; + +_.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); +}; + +_.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; +}; + +_.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); +}; + +_.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); +}; + +_.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; +}; + +/* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ +_.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; +}; + +_.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; +})(); + +/** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ +_.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; +})(); + +_.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; +}; + +_.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; +}; + +_.UUID = (function() { + + // Time-based entropy + var T = function() { + var time = 1 * new Date(); // cross-browser version of Date.now() + var ticks; + if (win.performance && win.performance.now) { + ticks = win.performance.now(); + } else { + // fall back to busy loop + ticks = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (time == 1 * new Date()) { + ticks++; + } + } + return time.toString(16) + Math.floor(ticks).toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; +})(); + +// _.isBlockedUA() +// This is to block various web spiders from executing our JS and +// sending false tracking data +var BLOCKED_UA_STRS = [ + 'ahrefsbot', + 'ahrefssiteaudit', + 'baiduspider', + 'bingbot', + 'bingpreview', + 'chrome-lighthouse', + 'facebookexternal', + 'petalbot', + 'pinterest', + 'screaming frog', + 'yahoo! slurp', + 'yandexbot', + + // a whole bunch of goog-specific crawlers + // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers + 'adsbot-google', + 'apis-google', + 'duplexweb-google', + 'feedfetcher-google', + 'google favicon', + 'google web preview', + 'google-read-aloud', + 'googlebot', + 'googleweblight', + 'mediapartners-google', + 'storebot-google' +]; +_.isBlockedUA = function(ua) { + var i; + ua = ua.toLowerCase(); + for (i = 0; i < BLOCKED_UA_STRS.length; i++) { + if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { + return true; + } + } + return false; +}; + +/** + * @param {Object=} formdata + * @param {string=} arg_separator + */ +_.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); +}; + +_.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + var result = results[1]; + try { + result = decodeURIComponent(result); + } catch(err) { + console.error('Skipping decoding for malformed query param: ' + result); + } + return result.replace(/\+/g, ' '); + } +}; + + +// _.cookie +// Methods partially borrowed from quirksmode.org/js/cookies.html +_.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', + expires = '', + secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', expires = '', secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, is_cross_subdomain, domain_override) { + _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); + } +}; + +var _localStorageSupported = null; +var localStorageSupported = function(storage, forceCheck) { + if (_localStorageSupported !== null && !forceCheck) { + return _localStorageSupported; + } + + var supported = true; + try { + storage = storage || window.localStorage; + var key = '__mplss_' + cheap_guid(8), + val = 'xyz'; + storage.setItem(key, val); + if (storage.getItem(key) !== val) { + supported = false; + } + storage.removeItem(key); + } catch (err) { + supported = false; + } + + _localStorageSupported = supported; + return supported; +}; + +// _.localStorage +_.localStorage = { + is_supported: function(force_check) { + var supported = localStorageSupported(null, force_check); + if (!supported) { + console.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }, + + error: function(msg) { + console.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } +}; + +_.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...*)} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; +})(); + + +var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); + +_.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(TOKEN_MATCH_REGEX); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; +})(); + +var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; +var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; + +_.info = { + campaignParams: function(default_value) { + var kw = '', + params = {}; + _.each(CAMPAIGN_KEYWORDS, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } else if (default_value !== undefined) { + params[kwkey] = default_value; + } + }); + + return params; + }, + + clickParams: function() { + var id = '', + params = {}; + _.each(CLICK_IDS, function(idkey) { + id = _.getQueryParam(document$1.URL, idkey); + if (id.length) { + params[idkey] = id; + } + }); + + return params; + }, + + marketingParams: function() { + return _.extend(_.info.campaignParams(), _.info.clickParams()); + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'SamsungBrowser/')) { + // https://developer.samsung.com/internet/user-agent-string-format + return 'Samsung Internet'; + } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + currentUrl: function() { + return win.location.href; + }, + + properties: function(extra_props) { + if (typeof extra_props !== 'object') { + extra_props = {}; + } + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': _.info.currentUrl(), + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION, + '$insert_id': cheap_guid(), + 'time': _.timestamp() / 1000 // epoch time in seconds + }, _.strip_empty_properties(extra_props)); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) + }); + }, + + mpPageViewProperties: function() { + return _.strip_empty_properties({ + 'current_page_title': document$1.title, + 'current_domain': win.location.hostname, + 'current_url_path': win.location.pathname, + 'current_url_protocol': win.location.protocol, + 'current_url_search': win.location.search + }); + } +}; + +var cheap_guid = function(maxlen) { + var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); + return maxlen ? guid.substring(0, maxlen) : guid; +}; + +// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) +var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; +// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk +var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; +/** + * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For + * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we + * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). + * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy + * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel + * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date + * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK + * offers the 'cookie_domain' config option to set it explicitly. + * @example + * extract_domain('my.sub.example.com') + * // 'example.com' + */ +var extract_domain = function(hostname) { + var domain_regex = DOMAIN_MATCH_REGEX; + var parts = hostname.split('.'); + var tld = parts[parts.length - 1]; + if (tld.length > 4 || tld === 'com' || tld === 'org') { + domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; + } + var matches = hostname.match(domain_regex); + return matches ? matches[0] : ''; +}; + +var JSONStringify = null, JSONParse = null; +if (typeof JSON !== 'undefined') { + JSONStringify = JSON.stringify; + JSONParse = JSON.parse; +} +JSONStringify = JSONStringify || _.JSONEncode; +JSONParse = JSONParse || _.JSONDecode; + +// EXPORTS (for closure compiler) +_['toArray'] = _.toArray; +_['isObject'] = _.isObject; +_['JSONEncode'] = _.JSONEncode; +_['JSONDecode'] = _.JSONDecode; +_['isBlockedUA'] = _.isBlockedUA; +_['isEmptyObject'] = _.isEmptyObject; +_['info'] = _.info; +_['info']['device'] = _.info.device; +_['info']['browser'] = _.info.browser; +_['info']['browserVersion'] = _.info.browserVersion; +_['info']['properties'] = _.info.properties; + +/* eslint camelcase: "off" */ + +/** + * DomTracker Object + * @constructor + */ +var DomTracker = function() {}; + + +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; + +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; + +/** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback + */ +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; +}; + +/** + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; +}; + +DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; + +var logger$2 = console_with_prefix('lock'); + +/** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ +var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; +}; + +// pass in a specific pid to test contention scenarios; otherwise +// it is chosen randomly for each acquisition attempt +SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } +}; + +var logger$1 = console_with_prefix('batch'); + +/** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ +var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; +}; + +/** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ +RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ +RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; +}; + +/** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ +var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; +}; + +/** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ +RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); +}; + +// internal helper for RequestQueue.updatePayloads +var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; +}; + +/** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ +RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ +RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; +}; + +/** + * Serialize the given items array to localStorage. + */ +RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } +}; + +/** + * Clear out queues (memory and localStorage). + */ +RequestQueue.prototype.clear = function() { + this.memQueue = []; + this.storage.removeItem(this.storageKey); +}; + +// maximum interval between request retries after exponential backoff +var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + +var logger = console_with_prefix('batch'); + +/** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ +var RequestBatcher = function(storageKey, options) { + this.errorReporter = options.errorReporter; + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage + }); + + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; + + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; + + this.stopped = !this.libConfig['batch_autostart']; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; +}; + +/** + * Add one item to queue. + */ +RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.flushInterval, cb); +}; + +/** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ +RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); +}; + +/** + * Stop flushing batches. Can be restarted by calling start(). + */ +RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } +}; + +/** + * Clear out queue. + */ +RequestBatcher.prototype.clear = function() { + this.queue.clear(); +}; + +/** + * Restore batch size configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetBatchSize = function() { + this.batchSize = this.libConfig['batch_size']; +}; + +/** + * Restore flush interval time configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); +}; + +/** + * Schedule the next flush in the given number of milliseconds. + */ +RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.flushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + } +}; + +/** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ +RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var startTime = new Date().getTime(); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + res.xhr_req && + (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + var headers = res.xhr_req['responseHeaders']; + if (headers) { + var retryAfter = headers['Retry-After']; + if (retryAfter) { + retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; + } + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + this.flush(); // handle next batch if the queue isn't empty + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.stopAllBatching(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger.log('MIXPANEL REQUEST:', dataForRequest); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } +}; + +/** + * Log error to global logger and optional user-defined logger. + */ +RequestBatcher.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + if (this.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.errorReporter(msg, err); + } catch(err) { + logger.error(err); + } + } +}; + +/** + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. + */ + +/** + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + +/** Public **/ + +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} + +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type + */ +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} + +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} + +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} + +/** Private **/ + +/** + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage + */ +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} + +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} + +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} + +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; + + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); + + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; + } + + options = options || {}; + + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} + +/** + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; + + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } + + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +/* eslint camelcase: "off" */ + +/** @const */ var SET_ACTION = '$set'; +/** @const */ var SET_ONCE_ACTION = '$set_once'; +/** @const */ var UNSET_ACTION = '$unset'; +/** @const */ var ADD_ACTION = '$add'; +/** @const */ var APPEND_ACTION = '$append'; +/** @const */ var UNION_ACTION = '$union'; +/** @const */ var REMOVE_ACTION = '$remove'; +/** @const */ var DELETE_ACTION = '$delete'; + +// Common internal methods for mixpanel.people and mixpanel.group APIs. +// These methods shouldn't involve network I/O. +var apiActions = { + set_action: function(prop, to) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + } else { + $set[prop] = to; + } + + data[SET_ACTION] = $set; + return data; + }, + + unset_action: function(prop) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + return data; + }, + + set_once_action: function(prop, to) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return data; + }, + + union_action: function(list_name, values) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + return data; + }, + + append_action: function(list_name, value) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + return data; + }, + + remove_action: function(list_name, value) { + var data = {}; + var $remove = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $remove[k] = v; + } + }, this); + } else { + $remove[list_name] = value; + } + data[REMOVE_ACTION] = $remove; + return data; + }, + + delete_action: function() { + var data = {}; + data[DELETE_ACTION] = ''; + return data; + } +}; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel Group Object + * @constructor + */ +var MixpanelGroup = function() {}; + +_.extend(MixpanelGroup.prototype, apiActions); + +MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { + this._mixpanel = mixpanel_instance; + this._group_key = group_key; + this._group_id = group_id; +}; + +/** + * Set properties on a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Set properties on a group, only if they do not yet exist. + * This will not overwrite previous group property values, unlike + * group.set(). + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set_once({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, lists or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Unset properties on a group permanently. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').unset('Founded'); + * + * @param {String} prop The name of the property. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/** + * Merge a given list with a list-valued group property, excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); + * + * @param {String} list_name Name of the property. + * @param {Array} values Values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/** + * Permanently delete a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').delete(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { + // bracket notation above prevents a minification error related to reserved words + var data = this.delete_action(); + return this._send_request(data, callback); +}); + +/** + * Remove a property from a group. The value will be ignored if doesn't exist. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); + * + * @param {String} list_name Name of the property. + * @param {Object} value Value to remove from the given group property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +MixpanelGroup.prototype._send_request = function(data, callback) { + data['$group_key'] = this._group_key; + data['$group_id'] = this._group_id; + data['$token'] = this._get_config('token'); + + var date_encoded_data = _.encodeDates(data); + return this._mixpanel._track_or_batch({ + type: 'groups', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], + batcher: this._mixpanel.request_batchers.groups + }, callback); +}; + +MixpanelGroup.prototype._is_reserved_property = function(prop) { + return prop === '$group_key' || prop === '$group_id'; +}; + +MixpanelGroup.prototype._get_config = function(conf) { + return this._mixpanel.get_config(conf); +}; + +MixpanelGroup.prototype.toString = function() { + return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; +}; + +// MixpanelGroup Exports +MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; +MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; +MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; +MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; +MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; +MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel People Object + * @constructor + */ +var MixpanelPeople = function() {}; + +_.extend(MixpanelPeople.prototype, apiActions); + +MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; +}; + +/* +* Set properties on a user record. +* +* ### Usage: +* +* mixpanel.people.set('gender', 'm'); +* +* // or set multiple properties at once +* mixpanel.people.set({ +* 'Company': 'Acme', +* 'Plan': 'Premium', +* 'Upgrade date': new Date() +* }); +* // properties can be strings, integers, dates, or lists +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document.referrer); + } + + // update $set object with default people properties + data[SET_ACTION] = _.extend( + {}, + _.info.people_properties(), + data[SET_ACTION] + ); + return this._send_request(data, callback); +}); + +/* +* Set properties on a user record, only if they do not yet exist. +* This will not overwrite previous people property values, unlike +* people.set(). +* +* ### Usage: +* +* mixpanel.people.set_once('First Login Date', new Date()); +* +* // or set multiple properties at once +* mixpanel.people.set_once({ +* 'First Login Date': new Date(), +* 'Starting Plan': 'Premium' +* }); +* +* // properties can be strings, integers or dates +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/* +* Unset properties on a user record (permanently removes the properties and their values from a profile). +* +* ### Usage: +* +* mixpanel.people.unset('gender'); +* +* // or unset multiple properties at once +* mixpanel.people.unset(['gender', 'Company']); +* +* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/* +* Increment/decrement numeric people analytics properties. +* +* ### Usage: +* +* mixpanel.people.increment('page_views', 1); +* +* // or, for convenience, if you're just incrementing a counter by +* // 1, you can simply do +* mixpanel.people.increment('page_views'); +* +* // to decrement a counter, pass a negative number +* mixpanel.people.increment('credits_left', -1); +* +* // like mixpanel.people.set(), you can increment multiple +* // properties at once: +* mixpanel.people.increment({ +* counter1: 1, +* counter2: 6 +* }); +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. +* @param {Number} [by] An amount to increment the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); +}); + +/* +* Append a value to a list-valued people analytics property. +* +* ### Usage: +* +* // append a value to a list, creating it if needed +* mixpanel.people.append('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.append({ +* list1: 'bob', +* list2: 123 +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value An item to append to the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.append_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Remove a value from a list-valued people analytics property. +* +* ### Usage: +* +* mixpanel.people.remove('School', 'UCB'); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value Item to remove from the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Merge a given list with a list-valued people analytics property, +* excluding duplicate values. +* +* ### Usage: +* +* // merge a value to a list, creating it if needed +* mixpanel.people.union('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.union({ +* list1: 'bob', +* list2: 123 +* }); +* +* // like mixpanel.people.append(), you can append multiple +* // values to the same list: +* mixpanel.people.union({ +* list1: ['bob', 'billy'] +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] Value / values to merge with the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + * @deprecated + */ +MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); +}); + +/* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * @deprecated + */ +MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); +}; + +/* +* Permanently deletes the current people analytics profile from +* Mixpanel (using the current distinct_id). +* +* ### Usage: +* +* // remove the all data you have stored about the current user +* mixpanel.people.delete_user(); +* +*/ +MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); +}; + +MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; +}; + +MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + var device_id = this._mixpanel.get_property('$device_id'); + var user_id = this._mixpanel.get_property('$user_id'); + var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); + if (device_id) { + data['$device_id'] = device_id; + } + if (user_id) { + data['$user_id'] = user_id; + } + if (had_persisted_distinct_id) { + data['$had_persisted_distinct_id'] = had_persisted_distinct_id; + } + + var date_encoded_data = _.encodeDates(data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return _.truncate(date_encoded_data, 255); + } + + return this._mixpanel._track_or_batch({ + type: 'people', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], + batcher: this._mixpanel.request_batchers.people + }, callback); +}; + +MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); +}; + +MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; +}; + +// Queue up engage operations if identify hasn't been called yet. +MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (REMOVE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console.error('Invalid call to _enqueue():', data); + } +}; + +MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + _this._mixpanel['persistence'].save(); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } +}; + +// Flush queued engage operations - order does not matter, +// and there are network level race conditions anyway +MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + var _this = this; + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var append_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + $append_item = $append_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($append_item)) { + _this.append($append_item, append_callback); + } + } + } + + // same for $remove + var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { + var $remove_item; + var remove_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); + } + if (!_.isUndefined(_remove_callback)) { + _remove_callback(response, data); + } + }; + for (var j = $remove_queue.length - 1; j >= 0; j--) { + $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + $remove_item = $remove_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($remove_item)) { + _this.remove($remove_item, remove_callback); + } + } + } +}; + +MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; +}; + +// MixpanelPeople Exports +MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; +MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; +MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; +MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; +MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; +MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; +MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; +MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; +MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; +MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; +MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + +/* eslint camelcase: "off" */ + +/* + * Constants + */ +/** @const */ var SET_QUEUE_KEY = '__mps'; +/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; +/** @const */ var UNSET_QUEUE_KEY = '__mpus'; +/** @const */ var ADD_QUEUE_KEY = '__mpa'; +/** @const */ var APPEND_QUEUE_KEY = '__mpap'; +/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; +/** @const */ var UNION_QUEUE_KEY = '__mpu'; +// This key is deprecated, but we want to check for it to see whether aliasing is allowed. +/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; +/** @const */ var ALIAS_ID_KEY = '__alias'; +/** @const */ var EVENT_TIMERS_KEY = '__timers'; +/** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + REMOVE_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + EVENT_TIMERS_KEY +]; + +/** + * Mixpanel Persistence Object + * @constructor + */ +var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + if (storage_type === 'localStorage' && _.localStorage.is_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(); + this.save(); +}; + +MixpanelPersistence.prototype.properties = function() { + var p = {}; + + this.load(); + + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; +}; + +MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } +}; + +MixpanelPersistence.prototype.upgrade = function() { + var old_cookie, + old_localstorage; + + // if transferring from cookie to localStorage or vice-versa, copy existing + // super properties over to new storage mode + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } else if (this.storage === _.cookie) { + old_localstorage = _.localStorage.parse(this.name); + + _.localStorage.remove(this.name); + + if (old_localstorage) { + this.register_once(old_localstorage); + } + } +}; + +MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure, + this.cross_site, + this.cookie_domain + ); +}; + +MixpanelPersistence.prototype.load_prop = function(key) { + this.load(); + return this['props'][key]; +}; + +MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false, this.cookie_domain); + this.storage.remove(this.name, true, this.cookie_domain); +}; + +// removes the storage entry and deletes all loaded data +// forced name for tests +MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; +}; + +/** +* @param {Object} props +* @param {*=} default_value +* @param {number=} days +*/ +MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; +}; + +/** +* @param {Object} props +* @param {number=} days +*/ +MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + _.extend(this['props'], props); + this.save(); + + return true; + } + return false; +}; + +MixpanelPersistence.prototype.unregister = function(prop) { + this.load(); + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } +}; + +MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); +}; + +// EXPORTED METHOD, we test this directly. +MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); +}; + +MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); +}; + +MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cookie_domain(config['cookie_domain']); + this.set_cross_site(config['cross_site_cookie']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); +}; + +MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } else { + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { + if (cookie_domain !== this.cookie_domain) { + this.remove(); + this.cookie_domain = cookie_domain; + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_site = function(cross_site) { + if (cross_site !== this.cross_site) { + this.cross_site = cross_site; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; +}; + +MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + remove_q = this._get_or_create_queue(REMOVE_ACTION, []), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === REMOVE_QUEUE_KEY) { + remove_q.push(q_data); + this._pop_from_people_queue(APPEND_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console.log(data); + + this.save(); +}; + +MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this['props'][this._get_queue_key(queue)]; + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { + // list actions: only remove if both k+v match + // e.g. remove should not override append in a case like + // append({foo: 'bar'}); remove({foo: 'qux'}) + _.each(q, function(queued_action) { + if (queued_action[k] === v) { + delete queued_action[k]; + } + }); + } else { + delete q[k]; + } + }, this); + } +}; + +MixpanelPersistence.prototype.load_queue = function(queue) { + return this.load_prop(this._get_queue_key(queue)); +}; + +MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === REMOVE_ACTION) { + return REMOVE_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console.error('Invalid queue:', queue); + } +}; + +MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + return this['props'][key] || (this['props'][key] = default_val); +}; + +MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); +}; + +MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; +}; + +/* eslint camelcase: "off" */ + +/* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @output_file_name mixpanel-2.8.min.js +// ==/ClosureCompiler== + +/* +SIMPLE STYLE GUIDE: + +this.x === public function +this._x === internal - only use within this file +this.__x === private - only use within the class + +Globals should be all caps +*/ + +var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + +var mixpanel_master; // main mixpanel instance / object +var INIT_MODULE = 0; +var INIT_SNIPPET = 1; + +var IDENTITY_FUNC = function(x) {return x;}; +var NOOP_FUNC = function() {}; + +/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; +/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; +/** @const */ var PAYLOAD_TYPE_JSON = 'json'; +/** @const */ var DEVICE_ID_PREFIX = '$device:'; + + +/* + * Dynamic... constants? Is that an oxymoron? + */ +// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ +// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials +var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + +// IE<10 does not support cross-origin XHR's but script tags +// with defer won't block window.onload; ENQUEUE_REQUESTS +// should only be true for Opera<12 +var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + +// save reference to navigator.sendBeacon so it can be minified +var sendBeacon = null; +if (navigator['sendBeacon']) { + sendBeacon = function() { + // late reference to navigator.sendBeacon to allow patching/spying + return navigator['sendBeacon'].apply(navigator, arguments); + }; +} + +var DEFAULT_API_ROUTES = { + 'track': 'track/', + 'engage': 'engage/', + 'groups': 'groups/', + 'record': 'record/' +}; + +/* + * Module-level globals + */ +var DEFAULT_CONFIG = { + 'api_host': 'https://api-js.mixpanel.com', + 'api_routes': DEFAULT_API_ROUTES, + 'api_method': 'POST', + 'api_transport': 'XHR', + 'api_payload_format': PAYLOAD_TYPE_BASE64, + 'app_host': 'https://mixpanel.com', + 'cdn': 'https://cdn.mxpnl.com', + 'cross_site_cookie': false, + 'cross_subdomain_cookie': true, + 'error_reporter': NOOP_FUNC, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_domain': '', + 'cookie_name': '', + 'loaded': NOOP_FUNC, + 'mp_loader': null, + 'track_marketing': true, + 'track_pageview': false, + 'skip_first_touch_marketing': false, + 'store_google': true, + 'stop_utm_persistence': false, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'opt_out_tracking_by_default': false, + 'opt_out_persistence_by_default': false, + 'opt_out_tracking_persistence_type': 'localStorage', + 'opt_out_tracking_cookie_prefix': null, + 'property_blacklist': [], + 'xhr_headers': {}, // { header: value, header2: value } + 'ignore_dnt': false, + 'batch_requests': true, + 'batch_size': 50, + 'batch_flush_interval_ms': 5000, + 'batch_request_timeout_ms': 90000, + 'batch_autostart': true, + 'hooks': {}, + 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), + 'record_block_selector': 'img, video', + 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), + 'record_mask_text_selector': '*', + 'record_max_ms': MAX_RECORDING_MS, + 'record_sessions_percent': 0, + 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' +}; + +var DOM_LOADED = false; + +/** + * Mixpanel Library Object + * @constructor + */ +var MixpanelLib = function() {}; + + +/** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ +var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._cached_groups = {}; // cache groups in a pool + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + if (!instance.get_config('skip_first_touch_marketing')) { + // We need null UTM params in the object because + // UTM parameters act as a tuple. If any UTM param + // is present, then we set all UTM params including + // empty ones together + var utm_params = _.info.campaignParams(null); + var initial_utm_params = {}; + var has_utm = false; + _.each(utm_params, function(utm_value, utm_key) { + initial_utm_params['initial_' + utm_key] = utm_value; + if (utm_value) { + has_utm = true; + } + }); + if (has_utm) { + instance['people'].set_once(initial_utm_params); + } + } + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; +}; + +// Initialization methods + +/** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ +MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + this.report_error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; +}; + +// mixpanel._init(token:string, config:object, name:string) +// +// This function sets up the current instance of the mixpanel +// library. The difference between this method and the init(...) +// method is this one initializes the actual instance, whereas the +// init(...) method sets up a new library and calls _init on it. +// +MixpanelLib.prototype._init = function(token, config, name) { + config = config || {}; + + this['__loaded'] = true; + this['config'] = {}; + + var variable_features = {}; + + // default to JSON payload for standard mixpanel.com API hosts + if (!('api_payload_format' in config)) { + var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; + if (api_host.match(/\.mixpanel\.com/)) { + variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; + } + } + + this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = NOOP_FUNC; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + // set up request queueing/batching + this.request_batchers = {}; + this._batch_requests = this.get_config('batch_requests'); + if (this._batch_requests) { + if (!_.localStorage.is_supported(true) || !USE_XHR) { + this._batch_requests = false; + console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + _.each(this.get_batcher_configs(), function(batcher_config) { + console.log('Clearing batch queue ' + batcher_config.queue_key); + _.localStorage.remove(batcher_config.queue_key); + }); + } else { + this.init_batchers(); + if (sendBeacon && win.addEventListener) { + // Before page closes or hides (user tabs away etc), attempt to flush any events + // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, + // events will not be removed from the persistent store; if the site is loaded again, + // the events will be flushed again on startup and deduplicated on the Mixpanel server + // side. + // There is no reliable way to capture only page close events, so we lean on the + // visibilitychange and pagehide events as recommended at + // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. + // These events fire when the user clicks away from the current page/tab, so will occur + // more frequently than page unload, but are the only mechanism currently for capturing + // this scenario somewhat reliably. + var flush_on_unload = _.bind(function() { + if (!this.request_batchers.events.stopped) { + this.request_batchers.events.flush({unloading: true}); + } + }, this); + win.addEventListener('pagehide', function(ev) { + if (ev['persisted']) { + flush_on_unload(); + } + }); + win.addEventListener('visibilitychange', function() { + if (document$1['visibilityState'] === 'hidden') { + flush_on_unload(); + } + }); + } + } + } + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.unpersisted_superprops = {}; + this._gdpr_init(); + + var uuid = _.UUID(); + if (!this.get_distinct_id()) { + // There is no need to set the distinct id + // or the device id if something was already stored + // in the persitence + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); + } + + var track_pageview_option = this.get_config('track_pageview'); + if (track_pageview_option) { + this._init_url_change_tracking(track_pageview_option); + } + + if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { + this.start_session_recording(); + } +}; + +MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { + if (!win['MutationObserver']) { + console.critical('Browser does not support MutationObserver; skipping session recording'); + return; + } + + var handleLoadedRecorder = _.bind(function() { + this._recorder = this._recorder || new win['__mp_recorder'](this); + this._recorder['startRecording'](); + }, this); + + if (_.isUndefined(win['__mp_recorder'])) { + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); + } else { + handleLoadedRecorder(); + } +}); + +MixpanelLib.prototype.stop_session_recording = function () { + if (this._recorder) { + this._recorder['stopRecording'](); + } else { + console.critical('Session recorder module not loaded'); + } +}; + +MixpanelLib.prototype.get_session_recording_properties = function () { + var props = {}; + if (this._recorder) { + var replay_id = this._recorder['replayId']; + if (replay_id) { + props['$mp_replay_id'] = replay_id; + } + } + return props; +}; + +// Private methods + +MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + this._set_default_superprops(); + this['people'].set_once(this['persistence'].get_referrer_info()); + + // `store_google` is now deprecated and previously stored UTM parameters are cleared + // from persistence by default. + if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { + var utm_params = _.info.campaignParams(null); + _.each(utm_params, function(_utm_value, utm_key) { + // We need to unregister persisted UTM parameters so old values + // are not mixed with the new UTM parameters + this.unregister(utm_key); + }.bind(this)); + } +}; + +// update persistence with info on referrer, UTM params, etc +MixpanelLib.prototype._set_default_superprops = function() { + this['persistence'].update_search_keyword(document$1.referrer); + // Registering super properties for UTM persistence by 'store_google' is deprecated. + if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { + this.register(_.info.campaignParams()); + } + if (this.get_config('save_referrer')) { + this['persistence'].update_referrer_info(document$1.referrer); + } +}; + +MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + + if (!this.has_opted_out_tracking()) { + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + } + + delete this.__dom_loaded_queue; + delete this.__request_queue; +}; + +MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + this.report_error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); +}; + +MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { + var previous_tracked_url = ''; + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = _.info.currentUrl(); + } + + if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { + win.addEventListener('popstate', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + win.addEventListener('hashchange', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + var nativePushState = win.history.pushState; + if (typeof nativePushState === 'function') { + win.history.pushState = function(state, unused, url) { + nativePushState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + var nativeReplaceState = win.history.replaceState; + if (typeof nativeReplaceState === 'function') { + win.history.replaceState = function(state, unused, url) { + nativeReplaceState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + win.addEventListener('mp_locationchange', function() { + var current_url = _.info.currentUrl(); + var should_track = false; + if (track_pageview_option === 'full-url') { + should_track = current_url !== previous_tracked_url; + } else if (track_pageview_option === 'url-with-path-and-query-string') { + should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; + } else if (track_pageview_option === 'url-with-path') { + should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; + } + + if (should_track) { + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = current_url; + } + } + }.bind(this)); + } +}; + +/** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ +MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } +}; + +MixpanelLib.prototype._send_request = function(url, data, options, callback) { + var succeeded = true; + + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return succeeded; + } + + var DEFAULT_OPTIONS = { + method: this.get_config('api_method'), + transport: this.get_config('api_transport'), + verbose: this.get_config('verbose') + }; + var body_data = null; + + if (!callback && (_.isFunction(options) || typeof options === 'string')) { + callback = options; + options = null; + } + options = _.extend(DEFAULT_OPTIONS, options || {}); + if (!USE_XHR) { + options.method = 'GET'; + } + var use_post = options.method === 'POST'; + var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; + + // needed to correctly format responses + var verbose_mode = options.verbose; + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + + if (use_post) { + body_data = 'data=' + encodeURIComponent(data['data']); + delete data['data']; + } + + url += '?' + _.HTTPBuildQuery(data); + + var lib = this; + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (use_sendBeacon) { + try { + succeeded = sendBeacon(url, body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + try { + if (callback) { + callback(succeeded ? 1 : 0); + } + } catch (e) { + lib.report_error(e); + } + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + + var headers = this.get_config('xhr_headers'); + if (use_post) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + + return succeeded; +}; + +/** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ +MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (_.isArray(fn_name)) { + tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() + } else if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + if (_.isArray(item[0])) { + // chained call + var caller = context; + _.each(item, function(call) { + caller = caller[call[0]].apply(caller, call.slice(1)); + }); + } else { + this[item[0]].apply(this, item.slice(1)); + } + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); +}; + +// request queueing utils + +MixpanelLib.prototype.are_batchers_initialized = function() { + return !!this.request_batchers.events; +}; + +MixpanelLib.prototype.get_batcher_configs = function() { + var queue_prefix = '__mpq_' + this.get_config('token'); + var api_routes = this.get_config('api_routes'); + this._batcher_configs = this._batcher_configs || { + events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, + people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, + groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} + }; + return this._batcher_configs; +}; + +MixpanelLib.prototype.init_batchers = function() { + if (!this.are_batchers_initialized()) { + var batcher_for = _.bind(function(attrs) { + return new RequestBatcher( + attrs.queue_key, + { + libConfig: this['config'], + sendRequestFunc: _.bind(function(data, options, cb) { + this._send_request( + this.get_config('api_host') + attrs.endpoint, + this._encode_data_for_request(data), + options, + this._prepare_callback(cb, data) + ); + }, this), + beforeSendHook: _.bind(function(item) { + return this._run_hook('before_send_' + attrs.type, item); + }, this), + errorReporter: this.get_config('error_reporter'), + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + } + ); + }, this); + var batcher_configs = this.get_batcher_configs(); + this.request_batchers = { + events: batcher_for(batcher_configs.events), + people: batcher_for(batcher_configs.people), + groups: batcher_for(batcher_configs.groups) + }; + } + if (this.get_config('batch_autostart')) { + this.start_batch_senders(); + } +}; + +MixpanelLib.prototype.start_batch_senders = function() { + this._batchers_were_started = true; + if (this.are_batchers_initialized()) { + this._batch_requests = true; + _.each(this.request_batchers, function(batcher) { + batcher.start(); + }); + } +}; + +MixpanelLib.prototype.stop_batch_senders = function() { + this._batch_requests = false; + _.each(this.request_batchers, function(batcher) { + batcher.stop(); + batcher.clear(); + }); +}; + +/** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ +MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); +}; + +/** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ +MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } +}; + +MixpanelLib.prototype._encode_data_for_request = function(data) { + var encoded_data = _.JSONEncode(data); + if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { + encoded_data = _.base64Encode(encoded_data); + } + return {'data': encoded_data}; +}; + +// internal method for handling track vs batch-enqueue logic +MixpanelLib.prototype._track_or_batch = function(options, callback) { + var truncated_data = _.truncate(options.data, 255); + var endpoint = options.endpoint; + var batcher = options.batcher; + var should_send_immediately = options.should_send_immediately; + var send_request_options = options.send_request_options || {}; + callback = callback || NOOP_FUNC; + + var request_enqueued_or_initiated = true; + var send_request_immediately = _.bind(function() { + if (!send_request_options.skip_hooks) { + truncated_data = this._run_hook('before_send_' + options.type, truncated_data); + } + if (truncated_data) { + console.log('MIXPANEL REQUEST:'); + console.log(truncated_data); + return this._send_request( + endpoint, + this._encode_data_for_request(truncated_data), + send_request_options, + this._prepare_callback(callback, truncated_data) + ); + } else { + return null; + } + }, this); + + if (this._batch_requests && !should_send_immediately) { + batcher.enqueue(truncated_data, function(succeeded) { + if (succeeded) { + callback(1, truncated_data); + } else { + send_request_immediately(); + } + }); + } else { + request_enqueued_or_initiated = send_request_immediately(); + } + + return request_enqueued_or_initiated && truncated_data; +}; + +/** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // track an event using navigator.sendBeacon + * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this track request. + * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). + * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + var transport = options['transport']; // external API, don't minify 'transport' prop + if (transport) { + options.transport = transport; // 'transport' prop name can be minified internally + } + var should_send_immediately = options['send_immediately']; + if (typeof callback !== 'function') { + callback = NOOP_FUNC; + } + + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = _.extend({}, properties); + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + this._set_default_superprops(); + + var marketing_properties = this.get_config('track_marketing') + ? _.info.marketingParams() + : {}; + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties({'mp_loader': this.get_config('mp_loader')}), + marketing_properties, + this['persistence'].properties(), + this.unpersisted_superprops, + this.get_session_recording_properties(), + properties + ); + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + var ret = this._track_or_batch({ + type: 'events', + data: data, + endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], + batcher: this.request_batchers.events, + should_send_immediately: should_send_immediately, + send_request_options: options + }, callback); + + return ret; +}); + +/** + * Register the current user into one/many groups. + * + * ### Usage: + * + * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs + * mixpanel.set_group('company', 'mixpanel') + * mixpanel.set_group('company', 128746312) + * + * @param {String} group_key Group key + * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * + */ +MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { + if (!_.isArray(group_ids)) { + group_ids = [group_ids]; + } + var prop = {}; + prop[group_key] = group_ids; + this.register(prop); + return this['people'].set(group_key, group_ids, callback); +}); + +/** + * Add a new group for this user. + * + * ### Usage: + * + * mixpanel.add_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_values = this.get_property(group_key); + var prop = {}; + if (old_values === undefined) { + prop[group_key] = [group_id]; + this.register(prop); + } else { + if (old_values.indexOf(group_id) === -1) { + old_values.push(group_id); + prop[group_key] = old_values; + this.register(prop); + } + } + return this['people'].union(group_key, group_id, callback); +}); + +/** + * Remove a group from this user. + * + * ### Usage: + * + * mixpanel.remove_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_value = this.get_property(group_key); + // if the value doesn't exist, the persistent store is unchanged + if (old_value !== undefined) { + var idx = old_value.indexOf(group_id); + if (idx > -1) { + old_value.splice(idx, 1); + this.register({group_key: old_value}); + } + if (old_value.length === 0) { + this.unregister(group_key); + } + } + return this['people'].remove(group_key, group_id, callback); +}); + +/** + * Track an event with specific groups. + * + * ### Usage: + * + * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) + * + * @param {String} event_name The name of the event (see `mixpanel.track()`) + * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) + * @param {Object=} groups An object mapping group name keys to one or more values + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { + var tracking_props = _.extend({}, properties || {}); + _.each(groups, function(v, k) { + if (v !== null && v !== undefined) { + tracking_props[k] = v; + } + }); + return this.track(event_name, tracking_props, callback); +}); + +MixpanelLib.prototype._create_map_key = function (group_key, group_id) { + return group_key + '_' + JSON.stringify(group_id); +}; + +MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { + delete this._cached_groups[this._create_map_key(group_key, group_id)]; +}; + +/** + * Look up reference to a Mixpanel group + * + * ### Usage: + * + * mixpanel.get_group(group_key, group_id) + * + * @param {String} group_key Group key + * @param {Object} group_id A valid Mixpanel property type + * @returns {Object} A MixpanelGroup identifier + */ +MixpanelLib.prototype.get_group = function (group_key, group_id) { + var map_key = this._create_map_key(group_key, group_id); + var group = this._cached_groups[map_key]; + if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { + group = new MixpanelGroup(); + group._init(this, group_key, group_id); + this._cached_groups[map_key] = group; + } + return group; +}; + +/** + * Track a default Mixpanel page view event, which includes extra default event properties to + * improve page view data. + * + * ### Usage: + * + * // track a default $mp_web_page_view event + * mixpanel.track_pageview(); + * + * // track a page view event with additional event properties + * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); + * + * // example approach to track page views on different page types as event properties + * mixpanel.track_pageview({'page': 'pricing'}); + * mixpanel.track_pageview({'page': 'homepage'}); + * + * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for + * // individual pages on the same site or product. Use cases for custom event_name may be page + * // views on different products or internal applications that are considered completely separate + * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); + * + * ### Notes: + * + * The `config.track_pageview` option for mixpanel.init() + * may be turned on for tracking page loads automatically. + * + * // track only page loads + * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); + * + * // track when the URL changes in any manner + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); + * + * // track when the URL changes, ignoring any changes in the hash part + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); + * + * // track when the path changes, ignoring any query parameter or hash changes + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); + * + * @param {Object} [properties] An optional set of additional properties to send with the page view event + * @param {Object} [options] Page view tracking options + * @param {String} [options.event_name] - Alternate name for the tracking event + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { + if (typeof properties !== 'object') { + properties = {}; + } + options = options || {}; + var event_name = options['event_name'] || '$mp_web_page_view'; + + var default_page_properties = _.extend( + _.info.mpPageViewProperties(), + _.info.campaignParams(), + _.info.clickParams() + ); + + var event_properties = _.extend( + {}, + default_page_properties, + properties + ); + + return this.track(event_name, event_properties); +}); + +/** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ +MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); +}; + +/** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ +MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); +}; + +/** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ +MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); +}; + +var REGISTER_DEFAULTS = { + 'persistent': true +}; +/** + * Helper to parse options param for register methods, maintaining + * legacy support for plain "days" param instead of options object + * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods + * @returns {Object} options object + */ +var options_for_register = function(days_or_options) { + var options; + if (_.isObject(days_or_options)) { + options = days_or_options; + } else if (!_.isUndefined(days_or_options)) { + options = {'days': days_or_options}; + } else { + options = {}; + } + return _.extend({}, REGISTER_DEFAULTS, options); +}; + +/** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * // register only for the current pageload + * mixpanel.register({'Name': 'Pat'}, {persistent: false}); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register = function(props, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register(props, options['days']); + } else { + _.extend(this.unpersisted_superprops, props); + } +}; + +/** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * // register once, only for the current pageload + * mixpanel.register_once({ + * 'First interaction time': new Date().toISOString() + * }, 'None', {persistent: false}); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register_once(props, default_value, options['days']); + } else { + if (typeof(default_value) === 'undefined') { + default_value = 'None'; + } + _.each(props, function(val, prop) { + if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { + this.unpersisted_superprops[prop] = val; + } + }, this); + } +}; + +/** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + * @param {Object} [options] + * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.unregister = function(property, options) { + options = options_for_register(options); + if (options['persistent']) { + this['persistence'].unregister(property); + } else { + delete this.unpersisted_superprops[property]; + } +}; + +MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); +}; + +/** + * Identify a user with a unique ID to track user activity across + * devices, tie a user to their events, and create a user profile. + * If you never call this method, unique visitors are tracked using + * a UUID generated the first time they visit the site. + * + * Call identify when you know the identity of the current user, + * typically after login or signup. We recommend against using + * identify for anonymous visitors to your site. + * + * ### Notes: + * If your project has + * ID Merge + * enabled, the identify method will connect pre- and + * post-authentication events when appropriate. + * + * If your project does not have ID Merge enabled, identify will + * change the user's local distinct_id to the unique ID you pass. + * Events tracked prior to authentication will not be connected + * to the same user identity. If ID Merge is disabled, alias can + * be used to connect pre- and post-registration events. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ +MixpanelLib.prototype.identify = function( + new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + var previous_distinct_id = this.get_distinct_id(); + if (new_distinct_id && previous_distinct_id !== new_distinct_id) { + // we allow the following condition if previous distinct_id is same as new_distinct_id + // so that you can force flush people updates for anonymous profiles. + if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { + this.report_error('distinct_id cannot have $device: prefix'); + return -1; + } + this.register({'$user_id': new_distinct_id}); + } + + if (!this.get_property('$device_id')) { + // The persisted distinct id might not actually be a device id at all + // it might be a distinct id of the user from before + var device_id = previous_distinct_id; + this.register_once({ + '$had_persisted_distinct_id': true, + '$device_id': device_id + }, ''); + } + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this.register({'distinct_id': new_distinct_id}); + } + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); + + // send an $identify event any time the distinct_id is changing - logic on the server + // will determine whether or not to do anything with it. + if (new_distinct_id !== previous_distinct_id) { + this.track('$identify', { + 'distinct_id': new_distinct_id, + '$anon_distinct_id': previous_distinct_id + }, {skip_hooks: true}); + } +}; + +/** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ +MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + var uuid = _.UUID(); + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); +}; + +/** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ +MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); +}; + +/** + * The alias method creates an alias which Mixpanel will use to + * remap one id to another. Multiple aliases can point to the + * same identifier. + * + * The following is a valid use of alias: + * + * mixpanel.alias('new_id', 'existing_id'); + * // You can add multiple id aliases to the existing ID + * mixpanel.alias('newer_id', 'existing_id'); + * + * Aliases can also be chained - the following is a valid example: + * + * mixpanel.alias('new_id', 'existing_id'); + * // chain newer_id - new_id - existing_id + * mixpanel.alias('newer_id', 'new_id'); + * + * Aliases cannot point to multiple identifiers - the following + * example will not work: + * + * mixpanel.alias('new_id', 'existing_id'); + * // this is invalid as 'new_id' already points to 'existing_id' + * mixpanel.alias('new_id', 'newer_id'); + * + * ### Notes: + * + * If your project does not have + * ID Merge + * enabled, the best practice is to call alias once when a unique + * ID is first created for a user (e.g., when a user first registers + * for an account). Do not use alias multiple times for a single + * user without ID Merge enabled. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ +MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + this.report_error('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { + 'alias': alias, + 'distinct_id': original + }, { + skip_hooks: true + }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + this.report_error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } +}; + +/** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @deprecated + */ +MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); +}; + +/** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // host for requests (customizable for e.g. a local proxy) + * api_host: 'https://api-js.mixpanel.com', + * + * // endpoints for different types of requests + * api_routes: { + * track: 'track/', + * engage: 'engage/', + * groups: 'groups/', + * } + * + * // HTTP method for tracking requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. Mixpanel + * // tracking via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // request-batching/queueing/retry + * batch_requests: true, + * + * // maximum number of events/updates to send in a single + * // network request + * batch_size: 50, + * + * // milliseconds to wait between sending batch requests + * batch_flush_interval_ms: 5000, + * + * // milliseconds to wait for network responses to batch requests + * // before they are considered timed-out and retried + * batch_request_timeout_ms: 90000, + * + * // override value for cookie domain, only useful for ensuring + * // correct cross-subdomain cookies on unusual domains like + * // subdomain.mainsite.avocat.fr; NB this cannot be used to + * // set cookies on a different domain than the current origin + * cookie_domain: '' + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // if true, cookie will be set with SameSite=None; Secure + * // this is only useful in special situations, like embedded + * // 3rd-party iframes that set up a Mixpanel instance + * cross_site_cookie: false + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of tracking by this Mixpanel instance by default + * opt_out_tracking_by_default: false + * + * // opt users out of browser data storage by this Mixpanel instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_tracking_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_tracking_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // disables enriching user profiles with first touch marketing data + * skip_first_touch_marketing: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // adds any UTM parameters and click IDs present on the page to any events fired + * track_marketing: true + * + * // enables automatic page view tracking using default page view events through + * // the track_pageview() method + * track_pageview: false + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // whether to ignore or respect the web browser's Do Not Track setting + * ignore_dnt: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ +MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + var new_batch_size = config['batch_size']; + if (new_batch_size) { + _.each(this.request_batchers, function(batcher) { + batcher.resetBatchSize(); + }); + } + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } +}; + +/** + * returns the current config object for the library. + */ +MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; +}; + +/** + * Fetch a hook function from config, with safe default, and run it + * against the given arguments + * @param {string} hook_name which hook to retrieve + * @returns {any|null} return value of user-provided hook, or null if nothing was returned + */ +MixpanelLib.prototype._run_hook = function(hook_name) { + var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); + if (typeof ret === 'undefined') { + this.report_error(hook_name + ' hook did not return a value'); + ret = null; + } + return ret; +}; + +/** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ +MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence'].load_prop([property_name]); +}; + +MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; +}; + +MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); +}; + +// perform some housekeeping around GDPR opt-in/out state +MixpanelLib.prototype._gdpr_init = function() { + var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; + + // try to convert opt-in/out cookies to localStorage if possible + if (is_localStorage_requested && _.localStorage.is_supported()) { + if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { + this.opt_in_tracking({'enable_persistence': false}); + } + if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { + this.opt_out_tracking({'clear_persistence': false}); + } + this.clear_opt_in_out_tracking({ + 'persistence_type': 'cookie', + 'enable_persistence': false + }); + } + + // check whether the user has already opted out - if so, clear & disable persistence + if (this.has_opted_out_tracking()) { + this._gdpr_update_persistence({'clear_persistence': true}); + + // check whether we should opt out by default + // note: we don't clear persistence here by default since opt-out default state is often + // used as an initial state while GDPR information is being collected + } else if (!this.has_opted_in_tracking() && ( + this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') + )) { + _.cookie.remove('mp_optout'); + this.opt_out_tracking({ + 'clear_persistence': this.get_config('opt_out_persistence_by_default') + }); + } +}; + +/** + * Enable or disable persistence based on options + * only enable/disable if persistence is not already in this state + * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it + * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence + */ +MixpanelLib.prototype._gdpr_update_persistence = function(options) { + var disabled; + if (options && options['clear_persistence']) { + disabled = true; + } else if (options && options['enable_persistence']) { + disabled = false; + } else { + return; + } + + if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { + this['persistence'].set_disabled(disabled); + } + + if (disabled) { + this.stop_batch_senders(); + } else { + // only start batchers after opt-in if they have previously been started + // in order to avoid unintentionally starting up batching for the first time + if (this._batchers_were_started) { + this.start_batch_senders(); + } + } +}; + +// call a base gdpr function after constructing the appropriate token and options args +MixpanelLib.prototype._gdpr_call_func = function(func, options) { + options = _.extend({ + 'track': _.bind(this.track, this), + 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), + 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), + 'cookie_expiration': this.get_config('cookie_expiration'), + 'cross_site_cookie': this.get_config('cross_site_cookie'), + 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), + 'cookie_domain': this.get_config('cookie_domain'), + 'secure_cookie': this.get_config('secure_cookie'), + 'ignore_dnt': this.get_config('ignore_dnt') + }, options); + + // check if localStorage can be used for recording opt out status, fall back to cookie if not + if (!_.localStorage.is_supported()) { + options['persistence_type'] = 'cookie'; + } + + return func(this.get_config('token'), { + track: options['track'], + trackEventName: options['track_event_name'], + trackProperties: options['track_properties'], + persistenceType: options['persistence_type'], + persistencePrefix: options['cookie_prefix'], + cookieDomain: options['cookie_domain'], + cookieExpiration: options['cookie_expiration'], + crossSiteCookie: options['cross_site_cookie'], + crossSubdomainCookie: options['cross_subdomain_cookie'], + secureCookie: options['secure_cookie'], + ignoreDnt: options['ignore_dnt'] + }); +}; + +/** + * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user in + * mixpanel.opt_in_tracking(); + * + * // opt user in with specific event name, properties, cookie configuration + * mixpanel.opt_in_tracking({ + * track_event_name: 'User opted in', + * track_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) + * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action + * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_in_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(optIn, options); + this._gdpr_update_persistence(options); +}; + +/** + * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user out + * mixpanel.opt_out_tracking(); + * + * // opt user out with different cookie configuration from Mixpanel instance + * mixpanel.opt_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_out_tracking = function(options) { + options = _.extend({ + 'clear_persistence': true, + 'delete_user': true + }, options); + + // delete user and clear charges since these methods may be disabled by opt-out + if (options['delete_user'] && this['people'] && this['people']._identify_called()) { + this['people'].delete_user(); + this['people'].clear_charges(); + } + + this._gdpr_call_func(optOut, options); + this._gdpr_update_persistence(options); +}; + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_in = mixpanel.has_opted_in_tracking(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ +MixpanelLib.prototype.has_opted_in_tracking = function(options) { + return this._gdpr_call_func(hasOptedIn, options); +}; + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_out = mixpanel.has_opted_out_tracking(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ +MixpanelLib.prototype.has_opted_out_tracking = function(options) { + return this._gdpr_call_func(hasOptedOut, options); +}; + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // clear user's opt-in/out status + * mixpanel.clear_opt_in_out_tracking(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_tracking/opt_out_tracking methods were called. + * mixpanel.clear_opt_in_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(clearOptInOut, options); + this._gdpr_update_persistence(options); +}; + +MixpanelLib.prototype.report_error = function(msg, err) { + console.error.apply(console.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + console.error(err); + } +}; + +// EXPORTS (for closure compiler) + +// MixpanelLib Exports +MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; +MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; +MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; +MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; +MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; +MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; +MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; +MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; +MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; +MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; +MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; +MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; +MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; +MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; +MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; +MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; +MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; +MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; +MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; +MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; +MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; +MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; +MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; +MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; +MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; +MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; +MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; +MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; +MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; +MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; +MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; +MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; +MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; +MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; +MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; + +// MixpanelPersistence Exports +MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; +MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; +MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; +MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; +MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + +var instances = {}; +var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; +}; + +var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; +}; + +var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); +}; + +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; + init_type = INIT_MODULE; + mixpanel_master = new MixpanelLib(); + + override_mp_init_func(); + mixpanel_master['init'](); + add_dom_loaded_handler(); + + return mixpanel_master; +} + +// For loading separate bundles asynchronously via script tag +// so that we don't load them until they are needed at runtime. +function loadAsync (src, onload) { + var scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + scriptEl.async = true; + scriptEl.onload = onload; + scriptEl.src = src; + document.head.appendChild(scriptEl); +} + +/* eslint camelcase: "off" */ + +var mixpanel = init_as_module(loadAsync); + +module.exports = mixpanel; diff --git a/rollup.config.js b/rollup.config.js index 42e72c0c..6f7b6053 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -8,4 +8,4 @@ export default { jsnext: true, }) ] -} +}; diff --git a/src/loaders/bundle-loaders.js b/src/loaders/bundle-loaders.js index 5763ef9d..754ebfda 100644 --- a/src/loaders/bundle-loaders.js +++ b/src/loaders/bundle-loaders.js @@ -18,5 +18,5 @@ export function loadNoop (_src, onload) { // and just the main SDK, throw an error when trying to load a separate bundle. // eslint-disable-next-line no-unused-vars export function loadThrowError (src, _onload) { - throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); + throw new Error('This build of Mixpanel only includes core SDK functionality, could not load ' + src); } diff --git a/src/loaders/loader-module-main.js b/src/loaders/loader-module-core.js similarity index 100% rename from src/loaders/loader-module-main.js rename to src/loaders/loader-module-core.js diff --git a/src/loaders/loader-module-bundle-async.js b/src/loaders/loader-module-with-async-recorder.js similarity index 100% rename from src/loaders/loader-module-bundle-async.js rename to src/loaders/loader-module-with-async-recorder.js From f47d79493bc875cad60880571cf715a05efb8fea Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 17 Jul 2024 17:23:09 +0000 Subject: [PATCH 41/48] core --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d4c1e48b..7a35026c 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ mixpanel.track("An event"); NOTE: the default `mixpanel-browser` bundle includes a bundled `mixpanel-recorder` SDK. We provide the following options to exclude `mixpanel-recorder` if you do not intend to use session replay or want to reduce bundle size: -To load the main SDK with no option of session recording: +To load the core SDK with no option of session recording: ```javascript import mixpanel from 'mixpanel-browser/src/loaders/loader-module-core'; ``` -To load the main SDK and optionally load session recording bundle asynchronously (via script tag): +To load the core SDK and optionally load session recording bundle asynchronously (via script tag): ```javascript import mixpanel from 'mixpanel-browser/src/loaders/loader-module-with-async-recorder'; ``` From dd9f5c33d03b44ae4b404fa4f0f31c4cc31349c3 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 17 Jul 2024 18:01:21 +0000 Subject: [PATCH 42/48] seq test --- src/recorder/index.js | 22 +++++++++++++------- tests/test.js | 48 +++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/recorder/index.js b/src/recorder/index.js index ccf67f9f..5716897a 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -129,6 +129,19 @@ MixpanelRecorder.prototype._onOptOut = function (code) { }; MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { 'method': 'POST', 'headers': { @@ -138,12 +151,7 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) 'body': reqBody, }).then(function (response) { response.json().then(function (responseBody) { - callback({ - status: 0, - httpStatusCode: response.status, - responseBody: responseBody, - retryAfter: response.headers.get('Retry-After') - }); + onSuccess(response, responseBody); }).catch(function (error) { callback({error: error}); }); @@ -165,7 +173,7 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da var reqParams = { 'distinct_id': String(this._mixpanel.get_distinct_id()), - 'seq': this.seqNo++, + 'seq': this.seqNo, 'batch_start_time': batchStartTime / 1000, 'replay_id': this.replayId, 'replay_length_ms': replayLengthMs, diff --git a/tests/test.js b/tests/test.js index 96b4524a..a02ae971 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5352,7 +5352,7 @@ ok(this.getRecorderScript() === null); }); - asyncTest('sends recording payload to server', 12, function () { + asyncTest('sends recording payload to server', 14, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null, "recorder script loaded"); @@ -5370,13 +5370,9 @@ var body = callArgs[1].body; same(body.constructor, Blob, 'request body is a Blob'); - var calledURL = callArgs[0]; - ok(calledURL.startsWith("https://api-js.mixpanel.com/record/")); - var paramsStr = calledURL.split('?')[1]; - var params = new URLSearchParams(paramsStr); - same(params.get('distinct_id'), mixpanel.recordertest.get_distinct_id()); - same(params.get('$device_id'), mixpanel.recordertest.get_property('$device_id')); + var urlParams1 = validateAndGetUrlParams(fetchCall1) + same(urlParams1.get("seq"), "0") simulateMouseClick(document.body); this.clock.tick(10 * 1000); @@ -5390,14 +5386,9 @@ var callArgs = fetchCall1.args; var body = callArgs[1].body; same(body.constructor, Blob, 'request body is a Blob'); - - var calledURL = callArgs[0]; - ok(calledURL.startsWith("https://api-js.mixpanel.com/record/")); - - var paramsStr = calledURL.split('?')[1]; - var params = new URLSearchParams(paramsStr); - same(params.get('distinct_id'), mixpanel.recordertest.get_distinct_id()); - same(params.get('$device_id'), mixpanel.recordertest.get_property('$device_id')); + + var urlParams2 = validateAndGetUrlParams(fetchCall2) + same(urlParams2.get("seq"), "1") mixpanel.recordertest.stop_session_recording(); done(); @@ -5526,7 +5517,7 @@ }); }); - asyncTest('retries record request after a 500', 14, function () { + asyncTest('retries record request after a 500', 17, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null); @@ -5544,22 +5535,25 @@ this.clock.tick(10 * 1000) same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); - validateAndGetUrlParams(this.fetchStub.getCall(0)); + var urlParams = validateAndGetUrlParams(this.fetchStub.getCall(0)); + same(urlParams.get("seq"), "0"); simulateMouseClick(document.body); this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); - validateAndGetUrlParams(this.fetchStub.getCall(1)); + urlParams = validateAndGetUrlParams(this.fetchStub.getCall(1)); + same(urlParams.get("seq"), "1"); this.clock.tick(20 * 2000); same(this.fetchStub.getCalls().length, 3, 'record request is retried after a 500'); - validateAndGetUrlParams(this.fetchStub.getCall(2)) + validateAndGetUrlParams(this.fetchStub.getCall(2)); + same(urlParams.get("seq"), "1"); mixpanel.recordertest.stop_session_recording(); }); }); - asyncTest('halves batch size and retries record request after a 413', 21, function () { + asyncTest('halves batch size and retries record request after a 413', 25, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); ok(this.getRecorderScript() !== null); @@ -5583,7 +5577,8 @@ this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); - validateAndGetUrlParams(this.fetchStub.getCall(0)); + var urlParams = validateAndGetUrlParams(this.fetchStub.getCall(0)); + same(urlParams.get("seq"), "0"); for (var _i = 0; _i < 1000; _i++) { simulateMouseClick(document.body); @@ -5591,15 +5586,20 @@ this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); - validateAndGetUrlParams(this.fetchStub.getCall(1)); + urlParams = validateAndGetUrlParams(this.fetchStub.getCall(1)); + same(urlParams.get("seq"), "1"); var events = JSON.parse(this.blobConstructorSpy.lastCall.args[0][0]) same(events.length, 1000); this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 4, 'record request is retried after a 413 and subsequently flushes the rest of events'); - validateAndGetUrlParams(this.fetchStub.getCall(2)); - validateAndGetUrlParams(this.fetchStub.getCall(3)); + + urlParams = validateAndGetUrlParams(this.fetchStub.getCall(2)); + same(urlParams.get("seq"), "1"); + + urlParams = validateAndGetUrlParams(this.fetchStub.getCall(3)); + same(urlParams.get("seq"), "2"); var numBlobCalls = this.blobConstructorSpy.getCalls().length From a9cabcb8f7670eeca25fcbdab019eead5a5de44d Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 17 Jul 2024 20:13:00 +0000 Subject: [PATCH 43/48] delete old bundles --- dist/mixpanel-bundle-async.cjs.js | 6332 ----------------------------- dist/mixpanel-main.cjs.js | 6330 ---------------------------- 2 files changed, 12662 deletions(-) delete mode 100644 dist/mixpanel-bundle-async.cjs.js delete mode 100644 dist/mixpanel-main.cjs.js diff --git a/dist/mixpanel-bundle-async.cjs.js b/dist/mixpanel-bundle-async.cjs.js deleted file mode 100644 index ba98f333..00000000 --- a/dist/mixpanel-bundle-async.cjs.js +++ /dev/null @@ -1,6332 +0,0 @@ -'use strict'; - -var Config = { - DEBUG: false, - LIB_VERSION: '2.53.0' -}; - -/* eslint camelcase: "off", eqeqeq: "off" */ - -// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file -var win; -if (typeof(window) === 'undefined') { - var loc = { - hostname: '' - }; - win = { - navigator: { userAgent: '' }, - document: { - location: loc, - referrer: '' - }, - screen: { width: 0, height: 0 }, - location: loc - }; -} else { - win = window; -} - -// Maximum allowed session recording length -var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours - -/* - * Saved references to long variable names, so that closure compiler can - * minimize file size. - */ - -var ArrayProto = Array.prototype, - FuncProto = Function.prototype, - ObjProto = Object.prototype, - slice = ArrayProto.slice, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty, - windowConsole = win.console, - navigator = win.navigator, - document$1 = win.document, - windowOpera = win.opera, - screen = win.screen, - userAgent = navigator.userAgent; - -var nativeBind = FuncProto.bind, - nativeForEach = ArrayProto.forEach, - nativeIndexOf = ArrayProto.indexOf, - nativeMap = ArrayProto.map, - nativeIsArray = Array.isArray, - breaker = {}; - -var _ = { - trim: function(str) { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill - return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); - } -}; - -// Console override -var console = { - /** @type {function(...*)} */ - log: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - try { - windowConsole.log.apply(windowConsole, arguments); - } catch (err) { - _.each(arguments, function(arg) { - windowConsole.log(arg); - }); - } - } - }, - /** @type {function(...*)} */ - warn: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); - try { - windowConsole.warn.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.warn(arg); - }); - } - } - }, - /** @type {function(...*)} */ - error: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } - }, - /** @type {function(...*)} */ - critical: function() { - if (!_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } - } -}; - -var log_func_with_prefix = function(func, prefix) { - return function() { - arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); - }; -}; -var console_with_prefix = function(prefix) { - return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) - }; -}; - - -// UNDERSCORE -// Embed part of the Underscore Library -_.bind = function(func, context) { - var args, bound; - if (nativeBind && func.bind === nativeBind) { - return nativeBind.apply(func, slice.call(arguments, 1)); - } - if (!_.isFunction(func)) { - throw new TypeError(); - } - args = slice.call(arguments, 2); - bound = function() { - if (!(this instanceof bound)) { - return func.apply(context, args.concat(slice.call(arguments))); - } - var ctor = {}; - ctor.prototype = func.prototype; - var self = new ctor(); - ctor.prototype = null; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) { - return result; - } - return self; - }; - return bound; -}; - -/** - * @param {*=} obj - * @param {function(...*)=} iterator - * @param {Object=} context - */ -_.each = function(obj, iterator, context) { - if (obj === null || obj === undefined) { - return; - } - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { - return; - } - } - } else { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) { - return; - } - } - } - } -}; - -_.extend = function(obj) { - _.each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (source[prop] !== void 0) { - obj[prop] = source[prop]; - } - } - }); - return obj; -}; - -_.isArray = nativeIsArray || function(obj) { - return toString.call(obj) === '[object Array]'; -}; - -// from a comment on http://dbj.org/dbj/?p=286 -// fails on only one very rare and deliberate custom object: -// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; -_.isFunction = function(f) { - try { - return /^\s*\bfunction\b/.test(f); - } catch (x) { - return false; - } -}; - -_.isArguments = function(obj) { - return !!(obj && hasOwnProperty.call(obj, 'callee')); -}; - -_.toArray = function(iterable) { - if (!iterable) { - return []; - } - if (iterable.toArray) { - return iterable.toArray(); - } - if (_.isArray(iterable)) { - return slice.call(iterable); - } - if (_.isArguments(iterable)) { - return slice.call(iterable); - } - return _.values(iterable); -}; - -_.map = function(arr, callback, context) { - if (nativeMap && arr.map === nativeMap) { - return arr.map(callback, context); - } else { - var results = []; - _.each(arr, function(item) { - results.push(callback.call(context, item)); - }); - return results; - } -}; - -_.keys = function(obj) { - var results = []; - if (obj === null) { - return results; - } - _.each(obj, function(value, key) { - results[results.length] = key; - }); - return results; -}; - -_.values = function(obj) { - var results = []; - if (obj === null) { - return results; - } - _.each(obj, function(value) { - results[results.length] = value; - }); - return results; -}; - -_.include = function(obj, target) { - var found = false; - if (obj === null) { - return found; - } - if (nativeIndexOf && obj.indexOf === nativeIndexOf) { - return obj.indexOf(target) != -1; - } - _.each(obj, function(value) { - if (found || (found = (value === target))) { - return breaker; - } - }); - return found; -}; - -_.includes = function(str, needle) { - return str.indexOf(needle) !== -1; -}; - -// Underscore Addons -_.inherit = function(subclass, superclass) { - subclass.prototype = new superclass(); - subclass.prototype.constructor = subclass; - subclass.superclass = superclass.prototype; - return subclass; -}; - -_.isObject = function(obj) { - return (obj === Object(obj) && !_.isArray(obj)); -}; - -_.isEmptyObject = function(obj) { - if (_.isObject(obj)) { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - return false; - } - } - return true; - } - return false; -}; - -_.isUndefined = function(obj) { - return obj === void 0; -}; - -_.isString = function(obj) { - return toString.call(obj) == '[object String]'; -}; - -_.isDate = function(obj) { - return toString.call(obj) == '[object Date]'; -}; - -_.isNumber = function(obj) { - return toString.call(obj) == '[object Number]'; -}; - -_.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); -}; - -_.encodeDates = function(obj) { - _.each(obj, function(v, k) { - if (_.isDate(v)) { - obj[k] = _.formatDate(v); - } else if (_.isObject(v)) { - obj[k] = _.encodeDates(v); // recurse - } - }); - return obj; -}; - -_.timestamp = function() { - Date.now = Date.now || function() { - return +new Date; - }; - return Date.now(); -}; - -_.formatDate = function(d) { - // YYYY-MM-DDTHH:MM:SS in UTC - function pad(n) { - return n < 10 ? '0' + n : n; - } - return d.getUTCFullYear() + '-' + - pad(d.getUTCMonth() + 1) + '-' + - pad(d.getUTCDate()) + 'T' + - pad(d.getUTCHours()) + ':' + - pad(d.getUTCMinutes()) + ':' + - pad(d.getUTCSeconds()); -}; - -_.strip_empty_properties = function(p) { - var ret = {}; - _.each(p, function(v, k) { - if (_.isString(v) && v.length > 0) { - ret[k] = v; - } - }); - return ret; -}; - -/* - * this function returns a copy of object after truncating it. If - * passed an Array or Object it will iterate through obj and - * truncate all the values recursively. - */ -_.truncate = function(obj, length) { - var ret; - - if (typeof(obj) === 'string') { - ret = obj.slice(0, length); - } else if (_.isArray(obj)) { - ret = []; - _.each(obj, function(val) { - ret.push(_.truncate(val, length)); - }); - } else if (_.isObject(obj)) { - ret = {}; - _.each(obj, function(val, key) { - ret[key] = _.truncate(val, length); - }); - } else { - ret = obj; - } - - return ret; -}; - -_.JSONEncode = (function() { - return function(mixed_val) { - var value = mixed_val; - var quote = function(string) { - var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex - var meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"': '\\"', - '\\': '\\\\' - }; - - escapable.lastIndex = 0; - return escapable.test(string) ? - '"' + string.replace(escapable, function(a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : - '"' + string + '"'; - }; - - var str = function(key, holder) { - var gap = ''; - var indent = ' '; - var i = 0; // The loop counter. - var k = ''; // The member key. - var v = ''; // The member value. - var length = 0; - var mind = gap; - var partial = []; - var value = holder[key]; - - // If the value has a toJSON method, call it to obtain a replacement value. - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - - // What happens next depends on the value's type. - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - // JSON numbers must be finite. Encode non-finite numbers as null. - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - // If the value is a boolean or null, convert it to a string. Note: - // typeof null does not produce 'null'. The case is included here in - // the remote chance that this gets fixed someday. - - return String(value); - - case 'object': - // If the type is 'object', we might be dealing with an object or an array or - // null. - // Due to a specification blunder in ECMAScript, typeof null is 'object', - // so watch out for that case. - if (!value) { - return 'null'; - } - - // Make an array to hold the partial results of stringifying this object value. - gap += indent; - partial = []; - - // Is the value an array? - if (toString.apply(value) === '[object Array]') { - // The value is an array. Stringify every element. Use null as a placeholder - // for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - - // Join all of the elements together, separated with commas, and wrap them in - // brackets. - v = partial.length === 0 ? '[]' : - gap ? '[\n' + gap + - partial.join(',\n' + gap) + '\n' + - mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - - // Iterate through all of the keys in the object. - for (k in value) { - if (hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - - // Join all of the member texts together, separated with commas, - // and wrap them in braces. - v = partial.length === 0 ? '{}' : - gap ? '{' + partial.join(',') + '' + - mind + '}' : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - }; - - // Make a fake root object containing our value under the key of ''. - // Return the result of stringifying the value. - return str('', { - '': value - }); - }; -})(); - -/** - * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js - * Slightly modified to throw a real Error rather than a POJO - */ -_.JSONDecode = (function() { - var at, // The index of the current character - ch, // The current character - escapee = { - '"': '"', - '\\': '\\', - '/': '/', - 'b': '\b', - 'f': '\f', - 'n': '\n', - 'r': '\r', - 't': '\t' - }, - text, - error = function(m) { - var e = new SyntaxError(m); - e.at = at; - e.text = text; - throw e; - }, - next = function(c) { - // If a c parameter is provided, verify that it matches the current character. - if (c && c !== ch) { - error('Expected \'' + c + '\' instead of \'' + ch + '\''); - } - // Get the next character. When there are no more characters, - // return the empty string. - ch = text.charAt(at); - at += 1; - return ch; - }, - number = function() { - // Parse a number value. - var number, - string = ''; - - if (ch === '-') { - string = '-'; - next('-'); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - if (ch === '.') { - string += '.'; - while (next() && ch >= '0' && ch <= '9') { - string += ch; - } - } - if (ch === 'e' || ch === 'E') { - string += ch; - next(); - if (ch === '-' || ch === '+') { - string += ch; - next(); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - } - number = +string; - if (!isFinite(number)) { - error('Bad number'); - } else { - return number; - } - }, - - string = function() { - // Parse a string value. - var hex, - i, - string = '', - uffff; - // When parsing for string values, we must look for " and \ characters. - if (ch === '"') { - while (next()) { - if (ch === '"') { - next(); - return string; - } - if (ch === '\\') { - next(); - if (ch === 'u') { - uffff = 0; - for (i = 0; i < 4; i += 1) { - hex = parseInt(next(), 16); - if (!isFinite(hex)) { - break; - } - uffff = uffff * 16 + hex; - } - string += String.fromCharCode(uffff); - } else if (typeof escapee[ch] === 'string') { - string += escapee[ch]; - } else { - break; - } - } else { - string += ch; - } - } - } - error('Bad string'); - }, - white = function() { - // Skip whitespace. - while (ch && ch <= ' ') { - next(); - } - }, - word = function() { - // true, false, or null. - switch (ch) { - case 't': - next('t'); - next('r'); - next('u'); - next('e'); - return true; - case 'f': - next('f'); - next('a'); - next('l'); - next('s'); - next('e'); - return false; - case 'n': - next('n'); - next('u'); - next('l'); - next('l'); - return null; - } - error('Unexpected "' + ch + '"'); - }, - value, // Placeholder for the value function. - array = function() { - // Parse an array value. - var array = []; - - if (ch === '[') { - next('['); - white(); - if (ch === ']') { - next(']'); - return array; // empty array - } - while (ch) { - array.push(value()); - white(); - if (ch === ']') { - next(']'); - return array; - } - next(','); - white(); - } - } - error('Bad array'); - }, - object = function() { - // Parse an object value. - var key, - object = {}; - - if (ch === '{') { - next('{'); - white(); - if (ch === '}') { - next('}'); - return object; // empty object - } - while (ch) { - key = string(); - white(); - next(':'); - if (Object.hasOwnProperty.call(object, key)) { - error('Duplicate key "' + key + '"'); - } - object[key] = value(); - white(); - if (ch === '}') { - next('}'); - return object; - } - next(','); - white(); - } - } - error('Bad object'); - }; - - value = function() { - // Parse a JSON value. It could be an object, an array, a string, - // a number, or a word. - white(); - switch (ch) { - case '{': - return object(); - case '[': - return array(); - case '"': - return string(); - case '-': - return number(); - default: - return ch >= '0' && ch <= '9' ? number() : word(); - } - }; - - // Return the json_parse function. It will have access to all of the - // above functions and variables. - return function(source) { - var result; - - text = source; - at = 0; - ch = ' '; - result = value(); - white(); - if (ch) { - error('Syntax error'); - } - - return result; - }; -})(); - -_.base64Encode = function(data) { - var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, - ac = 0, - enc = '', - tmp_arr = []; - - if (!data) { - return data; - } - - data = _.utf8Encode(data); - - do { // pack three octets into four hexets - o1 = data.charCodeAt(i++); - o2 = data.charCodeAt(i++); - o3 = data.charCodeAt(i++); - - bits = o1 << 16 | o2 << 8 | o3; - - h1 = bits >> 18 & 0x3f; - h2 = bits >> 12 & 0x3f; - h3 = bits >> 6 & 0x3f; - h4 = bits & 0x3f; - - // use hexets to index into b64, and append result to encoded string - tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); - } while (i < data.length); - - enc = tmp_arr.join(''); - - switch (data.length % 3) { - case 1: - enc = enc.slice(0, -2) + '=='; - break; - case 2: - enc = enc.slice(0, -1) + '='; - break; - } - - return enc; -}; - -_.utf8Encode = function(string) { - string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - var utftext = '', - start, - end; - var stringl = 0, - n; - - start = end = 0; - stringl = string.length; - - for (n = 0; n < stringl; n++) { - var c1 = string.charCodeAt(n); - var enc = null; - - if (c1 < 128) { - end++; - } else if ((c1 > 127) && (c1 < 2048)) { - enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); - } else { - enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); - } - if (enc !== null) { - if (end > start) { - utftext += string.substring(start, end); - } - utftext += enc; - start = end = n + 1; - } - } - - if (end > start) { - utftext += string.substring(start, string.length); - } - - return utftext; -}; - -_.UUID = (function() { - - // Time-based entropy - var T = function() { - var time = 1 * new Date(); // cross-browser version of Date.now() - var ticks; - if (win.performance && win.performance.now) { - ticks = win.performance.now(); - } else { - // fall back to busy loop - ticks = 0; - - // this while loop figures how many browser ticks go by - // before 1*new Date() returns a new number, ie the amount - // of ticks that go by per millisecond - while (time == 1 * new Date()) { - ticks++; - } - } - return time.toString(16) + Math.floor(ticks).toString(16); - }; - - // Math.Random entropy - var R = function() { - return Math.random().toString(16).replace('.', ''); - }; - - // User agent entropy - // This function takes the user agent string, and then xors - // together each sequence of 8 bytes. This produces a final - // sequence of 8 bytes which it returns as hex. - var UA = function() { - var ua = userAgent, - i, ch, buffer = [], - ret = 0; - - function xor(result, byte_array) { - var j, tmp = 0; - for (j = 0; j < byte_array.length; j++) { - tmp |= (buffer[j] << j * 8); - } - return result ^ tmp; - } - - for (i = 0; i < ua.length; i++) { - ch = ua.charCodeAt(i); - buffer.unshift(ch & 0xFF); - if (buffer.length >= 4) { - ret = xor(ret, buffer); - buffer = []; - } - } - - if (buffer.length > 0) { - ret = xor(ret, buffer); - } - - return ret.toString(16); - }; - - return function() { - var se = (screen.height * screen.width).toString(16); - return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); - }; -})(); - -// _.isBlockedUA() -// This is to block various web spiders from executing our JS and -// sending false tracking data -var BLOCKED_UA_STRS = [ - 'ahrefsbot', - 'ahrefssiteaudit', - 'baiduspider', - 'bingbot', - 'bingpreview', - 'chrome-lighthouse', - 'facebookexternal', - 'petalbot', - 'pinterest', - 'screaming frog', - 'yahoo! slurp', - 'yandexbot', - - // a whole bunch of goog-specific crawlers - // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers - 'adsbot-google', - 'apis-google', - 'duplexweb-google', - 'feedfetcher-google', - 'google favicon', - 'google web preview', - 'google-read-aloud', - 'googlebot', - 'googleweblight', - 'mediapartners-google', - 'storebot-google' -]; -_.isBlockedUA = function(ua) { - var i; - ua = ua.toLowerCase(); - for (i = 0; i < BLOCKED_UA_STRS.length; i++) { - if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { - return true; - } - } - return false; -}; - -/** - * @param {Object=} formdata - * @param {string=} arg_separator - */ -_.HTTPBuildQuery = function(formdata, arg_separator) { - var use_val, use_key, tmp_arr = []; - - if (_.isUndefined(arg_separator)) { - arg_separator = '&'; - } - - _.each(formdata, function(val, key) { - use_val = encodeURIComponent(val.toString()); - use_key = encodeURIComponent(key); - tmp_arr[tmp_arr.length] = use_key + '=' + use_val; - }); - - return tmp_arr.join(arg_separator); -}; - -_.getQueryParam = function(url, param) { - // Expects a raw URL - - param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); - var regexS = '[\\?&]' + param + '=([^&#]*)', - regex = new RegExp(regexS), - results = regex.exec(url); - if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { - return ''; - } else { - var result = results[1]; - try { - result = decodeURIComponent(result); - } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); - } - return result.replace(/\+/g, ' '); - } -}; - - -// _.cookie -// Methods partially borrowed from quirksmode.org/js/cookies.html -_.cookie = { - get: function(name) { - var nameEQ = name + '='; - var ca = document$1.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1, c.length); - } - if (c.indexOf(nameEQ) === 0) { - return decodeURIComponent(c.substring(nameEQ.length, c.length)); - } - } - return null; - }, - - parse: function(name) { - var cookie; - try { - cookie = _.JSONDecode(_.cookie.get(name)) || {}; - } catch (err) { - // noop - } - return cookie; - }, - - set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { - var cdomain = '', - expires = '', - secure = ''; - - if (domain_override) { - cdomain = '; domain=' + domain_override; - } else if (is_cross_subdomain) { - var domain = extract_domain(document$1.location.hostname); - cdomain = domain ? '; domain=.' + domain : ''; - } - - if (seconds) { - var date = new Date(); - date.setTime(date.getTime() + (seconds * 1000)); - expires = '; expires=' + date.toGMTString(); - } - - if (is_cross_site) { - is_secure = true; - secure = '; SameSite=None'; - } - if (is_secure) { - secure += '; secure'; - } - - document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; - }, - - set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { - var cdomain = '', expires = '', secure = ''; - - if (domain_override) { - cdomain = '; domain=' + domain_override; - } else if (is_cross_subdomain) { - var domain = extract_domain(document$1.location.hostname); - cdomain = domain ? '; domain=.' + domain : ''; - } - - if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = '; expires=' + date.toGMTString(); - } - - if (is_cross_site) { - is_secure = true; - secure = '; SameSite=None'; - } - if (is_secure) { - secure += '; secure'; - } - - var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; - document$1.cookie = new_cookie_val; - return new_cookie_val; - }, - - remove: function(name, is_cross_subdomain, domain_override) { - _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); - } -}; - -var _localStorageSupported = null; -var localStorageSupported = function(storage, forceCheck) { - if (_localStorageSupported !== null && !forceCheck) { - return _localStorageSupported; - } - - var supported = true; - try { - storage = storage || window.localStorage; - var key = '__mplss_' + cheap_guid(8), - val = 'xyz'; - storage.setItem(key, val); - if (storage.getItem(key) !== val) { - supported = false; - } - storage.removeItem(key); - } catch (err) { - supported = false; - } - - _localStorageSupported = supported; - return supported; -}; - -// _.localStorage -_.localStorage = { - is_supported: function(force_check) { - var supported = localStorageSupported(null, force_check); - if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); - } - return supported; - }, - - error: function(msg) { - console.error('localStorage error: ' + msg); - }, - - get: function(name) { - try { - return window.localStorage.getItem(name); - } catch (err) { - _.localStorage.error(err); - } - return null; - }, - - parse: function(name) { - try { - return _.JSONDecode(_.localStorage.get(name)) || {}; - } catch (err) { - // noop - } - return null; - }, - - set: function(name, value) { - try { - window.localStorage.setItem(name, value); - } catch (err) { - _.localStorage.error(err); - } - }, - - remove: function(name) { - try { - window.localStorage.removeItem(name); - } catch (err) { - _.localStorage.error(err); - } - } -}; - -_.register_event = (function() { - // written by Dean Edwards, 2005 - // with input from Tino Zijdel - crisp@xs4all.nl - // with input from Carl Sverre - mail@carlsverre.com - // with input from Mixpanel - // http://dean.edwards.name/weblog/2005/10/add-event/ - // https://gist.github.com/1930440 - - /** - * @param {Object} element - * @param {string} type - * @param {function(...*)} handler - * @param {boolean=} oldSchool - * @param {boolean=} useCapture - */ - var register_event = function(element, type, handler, oldSchool, useCapture) { - if (!element) { - console.error('No valid element provided to register_event'); - return; - } - - if (element.addEventListener && !oldSchool) { - element.addEventListener(type, handler, !!useCapture); - } else { - var ontype = 'on' + type; - var old_handler = element[ontype]; // can be undefined - element[ontype] = makeHandler(element, handler, old_handler); - } - }; - - function makeHandler(element, new_handler, old_handlers) { - var handler = function(event) { - event = event || fixEvent(window.event); - - // this basically happens in firefox whenever another script - // overwrites the onload callback and doesn't pass the event - // object to previously defined callbacks. All the browsers - // that don't define window.event implement addEventListener - // so the dom_loaded handler will still be fired as usual. - if (!event) { - return undefined; - } - - var ret = true; - var old_result, new_result; - - if (_.isFunction(old_handlers)) { - old_result = old_handlers(event); - } - new_result = new_handler.call(element, event); - - if ((false === old_result) || (false === new_result)) { - ret = false; - } - - return ret; - }; - - return handler; - } - - function fixEvent(event) { - if (event) { - event.preventDefault = fixEvent.preventDefault; - event.stopPropagation = fixEvent.stopPropagation; - } - return event; - } - fixEvent.preventDefault = function() { - this.returnValue = false; - }; - fixEvent.stopPropagation = function() { - this.cancelBubble = true; - }; - - return register_event; -})(); - - -var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); - -_.dom_query = (function() { - /* document.getElementsBySelector(selector) - - returns an array of element objects from the current document - matching the CSS selector. Selectors can contain element names, - class names and ids and can be nested. For example: - - elements = document.getElementsBySelector('div#main p a.external') - - Will return an array of all 'a' elements with 'external' in their - class attribute that are contained inside 'p' elements that are - contained inside the 'div' element which has id="main" - - New in version 0.4: Support for CSS2 and CSS3 attribute selectors: - See http://www.w3.org/TR/css3-selectors/#attribute-selectors - - Version 0.4 - Simon Willison, March 25th 2003 - -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows - -- Opera 7 fails - - Version 0.5 - Carl Sverre, Jan 7th 2013 - -- Now uses jQuery-esque `hasClass` for testing class name - equality. This fixes a bug related to '-' characters being - considered not part of a 'word' in regex. - */ - - function getAllChildren(e) { - // Returns all children of element. Workaround required for IE5/Windows. Ugh. - return e.all ? e.all : e.getElementsByTagName('*'); - } - - var bad_whitespace = /[\t\r\n]/g; - - function hasClass(elem, selector) { - var className = ' ' + selector + ' '; - return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); - } - - function getElementsBySelector(selector) { - // Attempt to fail gracefully in lesser browsers - if (!document$1.getElementsByTagName) { - return []; - } - // Split selector in to tokens - var tokens = selector.split(' '); - var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; - var currentContext = [document$1]; - for (i = 0; i < tokens.length; i++) { - token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); - if (token.indexOf('#') > -1) { - // Token is an ID selector - bits = token.split('#'); - tagName = bits[0]; - var id = bits[1]; - var element = document$1.getElementById(id); - if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { - // element not found or tag with that ID not found, return false - return []; - } - // Set currentContext to contain just this element - currentContext = [element]; - continue; // Skip to next token - } - if (token.indexOf('.') > -1) { - // Token contains a class selector - bits = token.split('.'); - tagName = bits[0]; - var className = bits[1]; - if (!tagName) { - tagName = '*'; - } - // Get elements matching tag, filter them for class selector - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - if (tagName == '*') { - elements = getAllChildren(currentContext[j]); - } else { - elements = currentContext[j].getElementsByTagName(tagName); - } - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = []; - currentContextIndex = 0; - for (j = 0; j < found.length; j++) { - if (found[j].className && - _.isString(found[j].className) && // some SVG elements have classNames which are not strings - hasClass(found[j], className) - ) { - currentContext[currentContextIndex++] = found[j]; - } - } - continue; // Skip to next token - } - // Code to deal with attribute selectors - var token_match = token.match(TOKEN_MATCH_REGEX); - if (token_match) { - tagName = token_match[1]; - var attrName = token_match[2]; - var attrOperator = token_match[3]; - var attrValue = token_match[4]; - if (!tagName) { - tagName = '*'; - } - // Grab all of the tagName elements within current context - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - if (tagName == '*') { - elements = getAllChildren(currentContext[j]); - } else { - elements = currentContext[j].getElementsByTagName(tagName); - } - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = []; - currentContextIndex = 0; - var checkFunction; // This function will be used to filter the elements - switch (attrOperator) { - case '=': // Equality - checkFunction = function(e) { - return (e.getAttribute(attrName) == attrValue); - }; - break; - case '~': // Match one of space seperated words - checkFunction = function(e) { - return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); - }; - break; - case '|': // Match start with value followed by optional hyphen - checkFunction = function(e) { - return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); - }; - break; - case '^': // Match starts with value - checkFunction = function(e) { - return (e.getAttribute(attrName).indexOf(attrValue) === 0); - }; - break; - case '$': // Match ends with value - fails with "Warning" in Opera 7 - checkFunction = function(e) { - return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); - }; - break; - case '*': // Match ends with value - checkFunction = function(e) { - return (e.getAttribute(attrName).indexOf(attrValue) > -1); - }; - break; - default: - // Just test for existence of attribute - checkFunction = function(e) { - return e.getAttribute(attrName); - }; - } - currentContext = []; - currentContextIndex = 0; - for (j = 0; j < found.length; j++) { - if (checkFunction(found[j])) { - currentContext[currentContextIndex++] = found[j]; - } - } - // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); - continue; // Skip to next token - } - // If we get here, token is JUST an element (not a class or ID selector) - tagName = token; - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - elements = currentContext[j].getElementsByTagName(tagName); - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = found; - } - return currentContext; - } - - return function(query) { - if (_.isElement(query)) { - return [query]; - } else if (_.isObject(query) && !_.isUndefined(query.length)) { - return query; - } else { - return getElementsBySelector.call(this, query); - } - }; -})(); - -var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; -var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; - -_.info = { - campaignParams: function(default_value) { - var kw = '', - params = {}; - _.each(CAMPAIGN_KEYWORDS, function(kwkey) { - kw = _.getQueryParam(document$1.URL, kwkey); - if (kw.length) { - params[kwkey] = kw; - } else if (default_value !== undefined) { - params[kwkey] = default_value; - } - }); - - return params; - }, - - clickParams: function() { - var id = '', - params = {}; - _.each(CLICK_IDS, function(idkey) { - id = _.getQueryParam(document$1.URL, idkey); - if (id.length) { - params[idkey] = id; - } - }); - - return params; - }, - - marketingParams: function() { - return _.extend(_.info.campaignParams(), _.info.clickParams()); - }, - - searchEngine: function(referrer) { - if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { - return 'google'; - } else if (referrer.search('https?://(.*)bing.com') === 0) { - return 'bing'; - } else if (referrer.search('https?://(.*)yahoo.com') === 0) { - return 'yahoo'; - } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { - return 'duckduckgo'; - } else { - return null; - } - }, - - searchInfo: function(referrer) { - var search = _.info.searchEngine(referrer), - param = (search != 'yahoo') ? 'q' : 'p', - ret = {}; - - if (search !== null) { - ret['$search_engine'] = search; - - var keyword = _.getQueryParam(referrer, param); - if (keyword.length) { - ret['mp_keyword'] = keyword; - } - } - - return ret; - }, - - /** - * This function detects which browser is running this script. - * The order of the checks are important since many user agents - * include key words used in later checks. - */ - browser: function(user_agent, vendor, opera) { - vendor = vendor || ''; // vendor is undefined for at least IE9 - if (opera || _.includes(user_agent, ' OPR/')) { - if (_.includes(user_agent, 'Mini')) { - return 'Opera Mini'; - } - return 'Opera'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { - return 'BlackBerry'; - } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { - return 'Internet Explorer Mobile'; - } else if (_.includes(user_agent, 'SamsungBrowser/')) { - // https://developer.samsung.com/internet/user-agent-string-format - return 'Samsung Internet'; - } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { - return 'Microsoft Edge'; - } else if (_.includes(user_agent, 'FBIOS')) { - return 'Facebook Mobile'; - } else if (_.includes(user_agent, 'Chrome')) { - return 'Chrome'; - } else if (_.includes(user_agent, 'CriOS')) { - return 'Chrome iOS'; - } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { - return 'UC Browser'; - } else if (_.includes(user_agent, 'FxiOS')) { - return 'Firefox iOS'; - } else if (_.includes(vendor, 'Apple')) { - if (_.includes(user_agent, 'Mobile')) { - return 'Mobile Safari'; - } - return 'Safari'; - } else if (_.includes(user_agent, 'Android')) { - return 'Android Mobile'; - } else if (_.includes(user_agent, 'Konqueror')) { - return 'Konqueror'; - } else if (_.includes(user_agent, 'Firefox')) { - return 'Firefox'; - } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { - return 'Internet Explorer'; - } else if (_.includes(user_agent, 'Gecko')) { - return 'Mozilla'; - } else { - return ''; - } - }, - - /** - * This function detects which browser version is running this script, - * parsing major and minor version (e.g., 42.1). User agent strings from: - * http://www.useragentstring.com/pages/useragentstring.php - */ - browserVersion: function(userAgent, vendor, opera) { - var browser = _.info.browser(userAgent, vendor, opera); - var versionRegexs = { - 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, - 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, - 'Chrome': /Chrome\/(\d+(\.\d+)?)/, - 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, - 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, - 'Safari': /Version\/(\d+(\.\d+)?)/, - 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, - 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, - 'Firefox': /Firefox\/(\d+(\.\d+)?)/, - 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, - 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, - 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, - 'Android Mobile': /android\s(\d+(\.\d+)?)/, - 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, - 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, - 'Mozilla': /rv:(\d+(\.\d+)?)/ - }; - var regex = versionRegexs[browser]; - if (regex === undefined) { - return null; - } - var matches = userAgent.match(regex); - if (!matches) { - return null; - } - return parseFloat(matches[matches.length - 2]); - }, - - os: function() { - var a = userAgent; - if (/Windows/i.test(a)) { - if (/Phone/.test(a) || /WPDesktop/.test(a)) { - return 'Windows Phone'; - } - return 'Windows'; - } else if (/(iPhone|iPad|iPod)/.test(a)) { - return 'iOS'; - } else if (/Android/.test(a)) { - return 'Android'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { - return 'BlackBerry'; - } else if (/Mac/i.test(a)) { - return 'Mac OS X'; - } else if (/Linux/.test(a)) { - return 'Linux'; - } else if (/CrOS/.test(a)) { - return 'Chrome OS'; - } else { - return ''; - } - }, - - device: function(user_agent) { - if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { - return 'Windows Phone'; - } else if (/iPad/.test(user_agent)) { - return 'iPad'; - } else if (/iPod/.test(user_agent)) { - return 'iPod Touch'; - } else if (/iPhone/.test(user_agent)) { - return 'iPhone'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { - return 'BlackBerry'; - } else if (/Android/.test(user_agent)) { - return 'Android'; - } else { - return ''; - } - }, - - referringDomain: function(referrer) { - var split = referrer.split('/'); - if (split.length >= 3) { - return split[2]; - } - return ''; - }, - - currentUrl: function() { - return win.location.href; - }, - - properties: function(extra_props) { - if (typeof extra_props !== 'object') { - extra_props = {}; - } - return _.extend(_.strip_empty_properties({ - '$os': _.info.os(), - '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), - '$referrer': document$1.referrer, - '$referring_domain': _.info.referringDomain(document$1.referrer), - '$device': _.info.device(userAgent) - }), { - '$current_url': _.info.currentUrl(), - '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), - '$screen_height': screen.height, - '$screen_width': screen.width, - 'mp_lib': 'web', - '$lib_version': Config.LIB_VERSION, - '$insert_id': cheap_guid(), - 'time': _.timestamp() / 1000 // epoch time in seconds - }, _.strip_empty_properties(extra_props)); - }, - - people_properties: function() { - return _.extend(_.strip_empty_properties({ - '$os': _.info.os(), - '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) - }), { - '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) - }); - }, - - mpPageViewProperties: function() { - return _.strip_empty_properties({ - 'current_page_title': document$1.title, - 'current_domain': win.location.hostname, - 'current_url_path': win.location.pathname, - 'current_url_protocol': win.location.protocol, - 'current_url_search': win.location.search - }); - } -}; - -var cheap_guid = function(maxlen) { - var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); - return maxlen ? guid.substring(0, maxlen) : guid; -}; - -// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) -var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; -// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk -var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; -/** - * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For - * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we - * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). - * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy - * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel - * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date - * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK - * offers the 'cookie_domain' config option to set it explicitly. - * @example - * extract_domain('my.sub.example.com') - * // 'example.com' - */ -var extract_domain = function(hostname) { - var domain_regex = DOMAIN_MATCH_REGEX; - var parts = hostname.split('.'); - var tld = parts[parts.length - 1]; - if (tld.length > 4 || tld === 'com' || tld === 'org') { - domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; - } - var matches = hostname.match(domain_regex); - return matches ? matches[0] : ''; -}; - -var JSONStringify = null, JSONParse = null; -if (typeof JSON !== 'undefined') { - JSONStringify = JSON.stringify; - JSONParse = JSON.parse; -} -JSONStringify = JSONStringify || _.JSONEncode; -JSONParse = JSONParse || _.JSONDecode; - -// EXPORTS (for closure compiler) -_['toArray'] = _.toArray; -_['isObject'] = _.isObject; -_['JSONEncode'] = _.JSONEncode; -_['JSONDecode'] = _.JSONDecode; -_['isBlockedUA'] = _.isBlockedUA; -_['isEmptyObject'] = _.isEmptyObject; -_['info'] = _.info; -_['info']['device'] = _.info.device; -_['info']['browser'] = _.info.browser; -_['info']['browserVersion'] = _.info.browserVersion; -_['info']['properties'] = _.info.properties; - -/* eslint camelcase: "off" */ - -/** - * DomTracker Object - * @constructor - */ -var DomTracker = function() {}; - - -// interface -DomTracker.prototype.create_properties = function() {}; -DomTracker.prototype.event_handler = function() {}; -DomTracker.prototype.after_track_handler = function() {}; - -DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; -}; - -/** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback - */ -DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - - that.event_handler(e, this, options); - - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); - - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); - - return true; -}; - -/** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured - */ -DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; - - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; - - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } - - that.after_track_handler(props, options, timeout_occured); - }; -}; - -DomTracker.prototype.create_properties = function(properties, element) { - var props; - - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } - - return props; -}; - -/** - * LinkTracker Object - * @constructor - * @extends DomTracker - */ -var LinkTracker = function() { - this.override_event = 'click'; -}; -_.inherit(LinkTracker, DomTracker); - -LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); - - if (element.href) { props['url'] = element.href; } - - return props; -}; - -LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; - - if (!options.new_tab) { - evt.preventDefault(); - } -}; - -LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } - - setTimeout(function() { - window.location = options.href; - }, 0); -}; - -/** - * FormTracker Object - * @constructor - * @extends DomTracker - */ -var FormTracker = function() { - this.override_event = 'submit'; -}; -_.inherit(FormTracker, DomTracker); - -FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); -}; - -FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); -}; - -var logger$2 = console_with_prefix('lock'); - -/** - * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser - * window/tab at a time will be able to access shared resources. - * - * Based on the Alur and Taubenfeld fast lock - * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) - * with an added timeout to ensure there will be eventual progress in the event - * that a window is closed in the middle of the callback. - * - * Implementation based on the original version by David Wolever (https://github.com/wolever) - * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. - * - * @example - * const myLock = new SharedLock('some-key'); - * myLock.withLock(function() { - * console.log('I hold the mutex!'); - * }); - * - * @constructor - */ -var SharedLock = function(key, options) { - options = options || {}; - - this.storageKey = key; - this.storage = options.storage || window.localStorage; - this.pollIntervalMS = options.pollIntervalMS || 100; - this.timeoutMS = options.timeoutMS || 2000; -}; - -// pass in a specific pid to test contention scenarios; otherwise -// it is chosen randomly for each acquisition attempt -SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { - if (!pid && typeof errorCB !== 'function') { - pid = errorCB; - errorCB = null; - } - - var i = pid || (new Date().getTime() + '|' + Math.random()); - var startTime = new Date().getTime(); - - var key = this.storageKey; - var pollIntervalMS = this.pollIntervalMS; - var timeoutMS = this.timeoutMS; - var storage = this.storage; - - var keyX = key + ':X'; - var keyY = key + ':Y'; - var keyZ = key + ':Z'; - - var reportError = function(err) { - errorCB && errorCB(err); - }; - - var delay = function(cb) { - if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); - storage.removeItem(keyZ); - storage.removeItem(keyY); - loop(); - return; - } - setTimeout(function() { - try { - cb(); - } catch(err) { - reportError(err); - } - }, pollIntervalMS * (Math.random() + 0.1)); - }; - - var waitFor = function(predicate, cb) { - if (predicate()) { - cb(); - } else { - delay(function() { - waitFor(predicate, cb); - }); - } - }; - - var getSetY = function() { - var valY = storage.getItem(keyY); - if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) - return false; - } else { - storage.setItem(keyY, i); - if (storage.getItem(keyY) === i) { - return true; - } else { - if (!localStorageSupported(storage, true)) { - throw new Error('localStorage support dropped while acquiring lock'); - } - return false; - } - } - }; - - var loop = function() { - storage.setItem(keyX, i); - - waitFor(getSetY, function() { - if (storage.getItem(keyX) === i) { - criticalSection(); - return; - } - - delay(function() { - if (storage.getItem(keyY) !== i) { - loop(); - return; - } - waitFor(function() { - return !storage.getItem(keyZ); - }, criticalSection); - }); - }); - }; - - var criticalSection = function() { - storage.setItem(keyZ, '1'); - try { - lockedCB(); - } finally { - storage.removeItem(keyZ); - if (storage.getItem(keyY) === i) { - storage.removeItem(keyY); - } - if (storage.getItem(keyX) === i) { - storage.removeItem(keyX); - } - } - }; - - try { - if (localStorageSupported(storage, true)) { - loop(); - } else { - throw new Error('localStorage support check failed'); - } - } catch(err) { - reportError(err); - } -}; - -var logger$1 = console_with_prefix('batch'); - -/** - * RequestQueue: queue for batching API requests with localStorage backup for retries. - * Maintains an in-memory queue which represents the source of truth for the current - * page, but also writes all items out to a copy in the browser's localStorage, which - * can be read on subsequent pageloads and retried. For batchability, all the request - * items in the queue should be of the same type (events, people updates, group updates) - * so they can be sent in a single request to the same API endpoint. - * - * LocalStorage keying and locking: In order for reloads and subsequent pageloads of - * the same site to access the same persisted data, they must share the same localStorage - * key (for instance based on project token and queue type). Therefore access to the - * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent - * simultaneously open windows/tabs from overwriting each other's data (which would lead - * to data loss in some situations). - * @constructor - */ -var RequestQueue = function(storageKey, options) { - options = options || {}; - this.storageKey = storageKey; - this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); - this.lock = new SharedLock(storageKey, {storage: this.storage}); - - this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios - - this.memQueue = []; -}; - -/** - * Add one item to queues (memory and localStorage). The queued entry includes - * the given item along with an auto-generated ID and a "flush-after" timestamp. - * It is expected that the item will be sent over the network and dequeued - * before the flush-after time; if this doesn't happen it is considered orphaned - * (e.g., the original tab where it was enqueued got closed before it could be - * sent) and the item can be sent by any tab that finds it in localStorage. - * - * The final callback param is called with a param indicating success or - * failure of the enqueue operation; it is asynchronous because the localStorage - * lock is asynchronous. - */ -RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { - var queueEntry = { - 'id': cheap_guid(), - 'flushAfter': new Date().getTime() + flushInterval * 2, - 'payload': item - }; - - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); -}; - -/** - * Read out the given number of queue entries. If this.memQueue - * has fewer than batchSize items, then look for "orphaned" items - * in the persisted queue (items where the 'flushAfter' time has - * already passed). - */ -RequestQueue.prototype.fillBatch = function(batchSize) { - var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { - // don't need lock just to read events; localStorage is thread-safe - // and the worst that could happen is a duplicate send of some - // orphaned events, which will be deduplicated on the server side - var storedQueue = this.readFromStorage(); - if (storedQueue.length) { - // item IDs already in batch; don't duplicate out of storage - var idsInBatch = {}; // poor man's Set - _.each(batch, function(item) { idsInBatch[item['id']] = true; }); - - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { - item.orphaned = true; - batch.push(item); - if (batch.length >= batchSize) { - break; - } - } - } - } - } - return batch; -}; - -/** - * Remove items with matching 'id' from array (immutably) - * also remove any item without a valid id (e.g., malformed - * storage entries). - */ -var filterOutIDsAndInvalid = function(items, idSet) { - var filteredItems = []; - _.each(items, function(item) { - if (item['id'] && !idSet[item['id']]) { - filteredItems.push(item); - } - }); - return filteredItems; -}; - -/** - * Remove items with matching IDs from both in-memory queue - * and persisted queue - */ -RequestQueue.prototype.removeItemsByID = function(ids, cb) { - var idSet = {}; // poor man's Set - _.each(ids, function(id) { idSet[id] = true; }); - - this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; - } - } - } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); - - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); - } - } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); -}; - -// internal helper for RequestQueue.updatePayloads -var updatePayloads = function(existingItems, itemsToUpdate) { - var newItems = []; - _.each(existingItems, function(item) { - var id = item['id']; - if (id in itemsToUpdate) { - var newPayload = itemsToUpdate[id]; - if (newPayload !== null) { - item['payload'] = newPayload; - newItems.push(item); - } - } else { - // no update - newItems.push(item); - } - }); - return newItems; -}; - -/** - * Update payloads of given items in both in-memory queue and - * persisted queue. Items set to null are removed from queues. - */ -RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { - this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); -}; - -/** - * Read and parse items array from localStorage entry, handling - * malformed/missing data if necessary. - */ -RequestQueue.prototype.readFromStorage = function() { - var storageEntry; - try { - storageEntry = this.storage.getItem(this.storageKey); - if (storageEntry) { - storageEntry = JSONParse(storageEntry); - if (!_.isArray(storageEntry)) { - this.reportError('Invalid storage entry:', storageEntry); - storageEntry = null; - } - } - } catch (err) { - this.reportError('Error retrieving queue', err); - storageEntry = null; - } - return storageEntry || []; -}; - -/** - * Serialize the given items array to localStorage. - */ -RequestQueue.prototype.saveToStorage = function(queue) { - try { - this.storage.setItem(this.storageKey, JSONStringify(queue)); - return true; - } catch (err) { - this.reportError('Error saving queue', err); - return false; - } -}; - -/** - * Clear out queues (memory and localStorage). - */ -RequestQueue.prototype.clear = function() { - this.memQueue = []; - this.storage.removeItem(this.storageKey); -}; - -// maximum interval between request retries after exponential backoff -var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - -var logger = console_with_prefix('batch'); - -/** - * RequestBatcher: manages the queueing, flushing, retry etc of requests of one - * type (events, people, groups). - * Uses RequestQueue to manage the backing store. - * @constructor - */ -var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; - this.queue = new RequestQueue(storageKey, { - errorReporter: _.bind(this.reportError, this), - storage: options.storage - }); - - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; - - this.stopped = !this.libConfig['batch_autostart']; - this.consecutiveRemovalFailures = 0; - - // extra client-side dedupe - this.itemIdsSentSuccessfully = {}; -}; - -/** - * Add one item to queue. - */ -RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); -}; - -/** - * Start flushing batches at the configured time interval. Must call - * this method upon SDK init in order to send anything over the network. - */ -RequestBatcher.prototype.start = function() { - this.stopped = false; - this.consecutiveRemovalFailures = 0; - this.flush(); -}; - -/** - * Stop flushing batches. Can be restarted by calling start(). - */ -RequestBatcher.prototype.stop = function() { - this.stopped = true; - if (this.timeoutID) { - clearTimeout(this.timeoutID); - this.timeoutID = null; - } -}; - -/** - * Clear out queue. - */ -RequestBatcher.prototype.clear = function() { - this.queue.clear(); -}; - -/** - * Restore batch size configuration to whatever is set in the main SDK. - */ -RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; -}; - -/** - * Restore flush interval time configuration to whatever is set in the main SDK. - */ -RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); -}; - -/** - * Schedule the next flush in the given number of milliseconds. - */ -RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; - if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); - } -}; - -/** - * Flush one batch to network. Depending on success/failure modes, it will either - * remove the batch from the queue or leave it in for retry, and schedule the next - * flush. In cases of most network or API failures, it will back off exponentially - * when retrying. - * @param {Object} [options] - * @param {boolean} [options.sendBeacon] - whether to send batch with - * navigator.sendBeacon (only useful for sending batches before page unloads, as - * sendBeacon offers no callbacks or status indications) - */ -RequestBatcher.prototype.flush = function(options) { - try { - - if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); - return; - } - - options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; - var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; - var batch = this.queue.fillBatch(currentBatchSize); - var dataForRequest = []; - var transformedItems = {}; - _.each(batch, function(item) { - var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); - } - if (payload) { - // mp_sent_by_lib_version prop captures which lib version actually - // sends each event (regardless of which version originally queued - // it for sending) - if (payload['event'] && payload['properties']) { - payload['properties'] = _.extend( - {}, - payload['properties'], - {'mp_sent_by_lib_version': Config.LIB_VERSION} - ); - } - var addPayload = true; - var itemId = item['id']; - if (itemId) { - if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { - this.reportError('[dupe] item ID sent too many times, not sending', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - addPayload = false; - } - } else { - this.reportError('[dupe] found item with no ID', {item: item}); - } - - if (addPayload) { - dataForRequest.push(payload); - } - } - transformedItems[item['id']] = payload; - }, this); - if (dataForRequest.length < 1) { - this.resetFlush(); - return; // nothing to do - } - - this.requestInProgress = true; - - var batchSendCallback = _.bind(function(res) { - this.requestInProgress = false; - - try { - - // handle API response in a try-catch to make sure we can reset the - // flush operation if something goes wrong - - var removeItemsFromQueue = false; - if (options.unloading) { - // update persisted data to include hook transformations - this.queue.updatePayloads(transformedItems); - } else if ( - _.isObject(res) && - res.error === 'timeout' && - new Date().getTime() - startTime >= timeoutMS - ) { - this.reportError('Network timeout; retrying'); - this.flush(); - } else if ( - _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') - ) { - // network or API error, or 429 Too Many Requests, retry - var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } - } - retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); - this.reportError('Error; retry in ' + retryMS + ' ms'); - this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { - // 413 Payload Too Large - if (batch.length > 1) { - var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); - this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); - this.reportError('413 response; reducing batch size to ' + this.batchSize); - this.resetFlush(); - } else { - this.reportError('Single-event request too large; dropping', batch); - this.resetBatchSize(); - removeItemsFromQueue = true; - } - } else { - // successful network request+response; remove each item in batch from queue - // (even if it was e.g. a 400, in which case retrying won't help) - removeItemsFromQueue = true; - } - - if (removeItemsFromQueue) { - this.queue.removeItemsByID( - _.map(batch, function(item) { return item['id']; }), - _.bind(function(succeeded) { - if (succeeded) { - this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty - } else { - this.reportError('Failed to remove items from queue'); - if (++this.consecutiveRemovalFailures > 5) { - this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); - } else { - this.resetFlush(); - } - } - }, this) - ); - - // client-side dedupe - _.each(batch, _.bind(function(item) { - var itemId = item['id']; - if (itemId) { - this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; - this.itemIdsSentSuccessfully[itemId]++; - if (this.itemIdsSentSuccessfully[itemId] > 5) { - this.reportError('[dupe] item ID sent too many times', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - } - } else { - this.reportError('[dupe] found item with no ID while removing', {item: item}); - } - }, this)); - } - - } catch(err) { - this.reportError('Error handling API response', err); - this.resetFlush(); - } - }, this); - var requestOptions = { - method: 'POST', - verbose: true, - ignore_json_errors: true, // eslint-disable-line camelcase - timeout_ms: timeoutMS // eslint-disable-line camelcase - }; - if (options.unloading) { - requestOptions.transport = 'sendBeacon'; - } - logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - - } catch(err) { - this.reportError('Error flushing request queue', err); - this.resetFlush(); - } -}; - -/** - * Log error to global logger and optional user-defined logger. - */ -RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); - if (this.errorReporter) { - try { - if (!(err instanceof Error)) { - err = new Error(msg); - } - this.errorReporter(msg, err); - } catch(err) { - logger.error(err); - } - } -}; - -/** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - -/** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ - -/** Public **/ - -var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - -/** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function optIn(token, options) { - _optInOut(true, token, options); -} - -/** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ -function optOut(token, options) { - _optInOut(false, token, options); -} - -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ -function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; -} - -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ -function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; - } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); - } - return optedOut; -} - -/** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); -} - -/** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); - }); -} - -/** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); - }); -} - -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); -} - -/** Private **/ - -/** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ -function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; -} - -/** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ -function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; -} - -/** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type - */ -function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); -} - -/** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ -function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); - - return hasDntOn; -} - -/** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); - return; - } - - options = options || {}; - - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); - - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true - }); - } -} - -/** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; - - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests - - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); - } - - if (!optedOut) { - return method.apply(this, arguments); - } - - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } - - return; - }; -} - -/* eslint camelcase: "off" */ - -/** @const */ var SET_ACTION = '$set'; -/** @const */ var SET_ONCE_ACTION = '$set_once'; -/** @const */ var UNSET_ACTION = '$unset'; -/** @const */ var ADD_ACTION = '$add'; -/** @const */ var APPEND_ACTION = '$append'; -/** @const */ var UNION_ACTION = '$union'; -/** @const */ var REMOVE_ACTION = '$remove'; -/** @const */ var DELETE_ACTION = '$delete'; - -// Common internal methods for mixpanel.people and mixpanel.group APIs. -// These methods shouldn't involve network I/O. -var apiActions = { - set_action: function(prop, to) { - var data = {}; - var $set = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - $set[k] = v; - } - }, this); - } else { - $set[prop] = to; - } - - data[SET_ACTION] = $set; - return data; - }, - - unset_action: function(prop) { - var data = {}; - var $unset = []; - if (!_.isArray(prop)) { - prop = [prop]; - } - - _.each(prop, function(k) { - if (!this._is_reserved_property(k)) { - $unset.push(k); - } - }, this); - - data[UNSET_ACTION] = $unset; - return data; - }, - - set_once_action: function(prop, to) { - var data = {}; - var $set_once = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - $set_once[k] = v; - } - }, this); - } else { - $set_once[prop] = to; - } - data[SET_ONCE_ACTION] = $set_once; - return data; - }, - - union_action: function(list_name, values) { - var data = {}; - var $union = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $union[k] = _.isArray(v) ? v : [v]; - } - }, this); - } else { - $union[list_name] = _.isArray(values) ? values : [values]; - } - data[UNION_ACTION] = $union; - return data; - }, - - append_action: function(list_name, value) { - var data = {}; - var $append = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $append[k] = v; - } - }, this); - } else { - $append[list_name] = value; - } - data[APPEND_ACTION] = $append; - return data; - }, - - remove_action: function(list_name, value) { - var data = {}; - var $remove = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $remove[k] = v; - } - }, this); - } else { - $remove[list_name] = value; - } - data[REMOVE_ACTION] = $remove; - return data; - }, - - delete_action: function() { - var data = {}; - data[DELETE_ACTION] = ''; - return data; - } -}; - -/* eslint camelcase: "off" */ - -/** - * Mixpanel Group Object - * @constructor - */ -var MixpanelGroup = function() {}; - -_.extend(MixpanelGroup.prototype, apiActions); - -MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { - this._mixpanel = mixpanel_instance; - this._group_key = group_key; - this._group_id = group_id; -}; - -/** - * Set properties on a group. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); - * - * // or set multiple properties at once - * mixpanel.get_group('company', 'mixpanel').set({ - * 'Location': '405 Howard', - * 'Founded' : 2009, - * }); - * // properties can be strings, integers, dates, or lists - * - * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. - * @param {*} [to] A value to set on the given property name - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { - var data = this.set_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/** - * Set properties on a group, only if they do not yet exist. - * This will not overwrite previous group property values, unlike - * group.set(). - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); - * - * // or set multiple properties at once - * mixpanel.get_group('company', 'mixpanel').set_once({ - * 'Location': '405 Howard', - * 'Founded' : 2009, - * }); - * // properties can be strings, integers, lists or dates - * - * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. - * @param {*} [to] A value to set on the given property name - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { - var data = this.set_once_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/** - * Unset properties on a group permanently. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').unset('Founded'); - * - * @param {String} prop The name of the property. - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { - var data = this.unset_action(prop); - return this._send_request(data, callback); -}); - -/** - * Merge a given list with a list-valued group property, excluding duplicate values. - * - * ### Usage: - * - * // merge a value to a list, creating it if needed - * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); - * - * @param {String} list_name Name of the property. - * @param {Array} values Values to merge with the given property - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { - if (_.isObject(list_name)) { - callback = values; - } - var data = this.union_action(list_name, values); - return this._send_request(data, callback); -}); - -/** - * Permanently delete a group. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').delete(); - * - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { - // bracket notation above prevents a minification error related to reserved words - var data = this.delete_action(); - return this._send_request(data, callback); -}); - -/** - * Remove a property from a group. The value will be ignored if doesn't exist. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); - * - * @param {String} list_name Name of the property. - * @param {Object} value Value to remove from the given group property - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { - var data = this.remove_action(list_name, value); - return this._send_request(data, callback); -}); - -MixpanelGroup.prototype._send_request = function(data, callback) { - data['$group_key'] = this._group_key; - data['$group_id'] = this._group_id; - data['$token'] = this._get_config('token'); - - var date_encoded_data = _.encodeDates(data); - return this._mixpanel._track_or_batch({ - type: 'groups', - data: date_encoded_data, - endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], - batcher: this._mixpanel.request_batchers.groups - }, callback); -}; - -MixpanelGroup.prototype._is_reserved_property = function(prop) { - return prop === '$group_key' || prop === '$group_id'; -}; - -MixpanelGroup.prototype._get_config = function(conf) { - return this._mixpanel.get_config(conf); -}; - -MixpanelGroup.prototype.toString = function() { - return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; -}; - -// MixpanelGroup Exports -MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; -MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; -MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; -MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; -MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; -MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; - -/* eslint camelcase: "off" */ - -/** - * Mixpanel People Object - * @constructor - */ -var MixpanelPeople = function() {}; - -_.extend(MixpanelPeople.prototype, apiActions); - -MixpanelPeople.prototype._init = function(mixpanel_instance) { - this._mixpanel = mixpanel_instance; -}; - -/* -* Set properties on a user record. -* -* ### Usage: -* -* mixpanel.people.set('gender', 'm'); -* -* // or set multiple properties at once -* mixpanel.people.set({ -* 'Company': 'Acme', -* 'Plan': 'Premium', -* 'Upgrade date': new Date() -* }); -* // properties can be strings, integers, dates, or lists -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [to] A value to set on the given property name -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { - var data = this.set_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - // make sure that the referrer info has been updated and saved - if (this._get_config('save_referrer')) { - this._mixpanel['persistence'].update_referrer_info(document.referrer); - } - - // update $set object with default people properties - data[SET_ACTION] = _.extend( - {}, - _.info.people_properties(), - data[SET_ACTION] - ); - return this._send_request(data, callback); -}); - -/* -* Set properties on a user record, only if they do not yet exist. -* This will not overwrite previous people property values, unlike -* people.set(). -* -* ### Usage: -* -* mixpanel.people.set_once('First Login Date', new Date()); -* -* // or set multiple properties at once -* mixpanel.people.set_once({ -* 'First Login Date': new Date(), -* 'Starting Plan': 'Premium' -* }); -* -* // properties can be strings, integers or dates -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [to] A value to set on the given property name -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { - var data = this.set_once_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/* -* Unset properties on a user record (permanently removes the properties and their values from a profile). -* -* ### Usage: -* -* mixpanel.people.unset('gender'); -* -* // or unset multiple properties at once -* mixpanel.people.unset(['gender', 'Company']); -* -* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { - var data = this.unset_action(prop); - return this._send_request(data, callback); -}); - -/* -* Increment/decrement numeric people analytics properties. -* -* ### Usage: -* -* mixpanel.people.increment('page_views', 1); -* -* // or, for convenience, if you're just incrementing a counter by -* // 1, you can simply do -* mixpanel.people.increment('page_views'); -* -* // to decrement a counter, pass a negative number -* mixpanel.people.increment('credits_left', -1); -* -* // like mixpanel.people.set(), you can increment multiple -* // properties at once: -* mixpanel.people.increment({ -* counter1: 1, -* counter2: 6 -* }); -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. -* @param {Number} [by] An amount to increment the given property -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { - var data = {}; - var $add = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); - return; - } else { - $add[k] = v; - } - } - }, this); - callback = by; - } else { - // convenience: mixpanel.people.increment('property'); will - // increment 'property' by 1 - if (_.isUndefined(by)) { - by = 1; - } - $add[prop] = by; - } - data[ADD_ACTION] = $add; - - return this._send_request(data, callback); -}); - -/* -* Append a value to a list-valued people analytics property. -* -* ### Usage: -* -* // append a value to a list, creating it if needed -* mixpanel.people.append('pages_visited', 'homepage'); -* -* // like mixpanel.people.set(), you can append multiple -* // properties at once: -* mixpanel.people.append({ -* list1: 'bob', -* list2: 123 -* }); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] value An item to append to the list -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { - if (_.isObject(list_name)) { - callback = value; - } - var data = this.append_action(list_name, value); - return this._send_request(data, callback); -}); - -/* -* Remove a value from a list-valued people analytics property. -* -* ### Usage: -* -* mixpanel.people.remove('School', 'UCB'); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] value Item to remove from the list -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { - if (_.isObject(list_name)) { - callback = value; - } - var data = this.remove_action(list_name, value); - return this._send_request(data, callback); -}); - -/* -* Merge a given list with a list-valued people analytics property, -* excluding duplicate values. -* -* ### Usage: -* -* // merge a value to a list, creating it if needed -* mixpanel.people.union('pages_visited', 'homepage'); -* -* // like mixpanel.people.set(), you can append multiple -* // properties at once: -* mixpanel.people.union({ -* list1: 'bob', -* list2: 123 -* }); -* -* // like mixpanel.people.append(), you can append multiple -* // values to the same list: -* mixpanel.people.union({ -* list1: ['bob', 'billy'] -* }); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] Value / values to merge with the given property -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { - if (_.isObject(list_name)) { - callback = values; - } - var data = this.union_action(list_name, values); - return this._send_request(data, callback); -}); - -/* - * Record that you have charged the current user a certain amount - * of money. Charges recorded with track_charge() will appear in the - * Mixpanel revenue report. - * - * ### Usage: - * - * // charge a user $50 - * mixpanel.people.track_charge(50); - * - * // charge a user $30.50 on the 2nd of january - * mixpanel.people.track_charge(30.50, { - * '$time': new Date('jan 1 2012') - * }); - * - * @param {Number} amount The amount of money charged to the current user - * @param {Object} [properties] An associative array of properties associated with the charge - * @param {Function} [callback] If provided, the callback will be called when the server responds - * @deprecated - */ -MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { - if (!_.isNumber(amount)) { - amount = parseFloat(amount); - if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); - return; - } - } - - return this.append('$transactions', _.extend({ - '$amount': amount - }, properties), callback); -}); - -/* - * Permanently clear all revenue report transactions from the - * current user's people analytics profile. - * - * ### Usage: - * - * mixpanel.people.clear_charges(); - * - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - * @deprecated - */ -MixpanelPeople.prototype.clear_charges = function(callback) { - return this.set('$transactions', [], callback); -}; - -/* -* Permanently deletes the current people analytics profile from -* Mixpanel (using the current distinct_id). -* -* ### Usage: -* -* // remove the all data you have stored about the current user -* mixpanel.people.delete_user(); -* -*/ -MixpanelPeople.prototype.delete_user = function() { - if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); - return; - } - var data = {'$delete': this._mixpanel.get_distinct_id()}; - return this._send_request(data); -}; - -MixpanelPeople.prototype.toString = function() { - return this._mixpanel.toString() + '.people'; -}; - -MixpanelPeople.prototype._send_request = function(data, callback) { - data['$token'] = this._get_config('token'); - data['$distinct_id'] = this._mixpanel.get_distinct_id(); - var device_id = this._mixpanel.get_property('$device_id'); - var user_id = this._mixpanel.get_property('$user_id'); - var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); - if (device_id) { - data['$device_id'] = device_id; - } - if (user_id) { - data['$user_id'] = user_id; - } - if (had_persisted_distinct_id) { - data['$had_persisted_distinct_id'] = had_persisted_distinct_id; - } - - var date_encoded_data = _.encodeDates(data); - - if (!this._identify_called()) { - this._enqueue(data); - if (!_.isUndefined(callback)) { - if (this._get_config('verbose')) { - callback({status: -1, error: null}); - } else { - callback(-1); - } - } - return _.truncate(date_encoded_data, 255); - } - - return this._mixpanel._track_or_batch({ - type: 'people', - data: date_encoded_data, - endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], - batcher: this._mixpanel.request_batchers.people - }, callback); -}; - -MixpanelPeople.prototype._get_config = function(conf_var) { - return this._mixpanel.get_config(conf_var); -}; - -MixpanelPeople.prototype._identify_called = function() { - return this._mixpanel._flags.identify_called === true; -}; - -// Queue up engage operations if identify hasn't been called yet. -MixpanelPeople.prototype._enqueue = function(data) { - if (SET_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); - } else if (SET_ONCE_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); - } else if (UNSET_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); - } else if (ADD_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); - } else if (APPEND_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); - } else if (REMOVE_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); - } else if (UNION_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); - } else { - console.error('Invalid call to _enqueue():', data); - } -}; - -MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { - var _this = this; - var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); - var action_params = queued_data; - - if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { - _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); - _this._mixpanel['persistence'].save(); - if (queue_to_params_fn) { - action_params = queue_to_params_fn(queued_data); - } - action_method.call(_this, action_params, function(response, data) { - // on bad response, we want to add it back to the queue - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); - } - if (!_.isUndefined(callback)) { - callback(response, data); - } - }); - } -}; - -// Flush queued engage operations - order does not matter, -// and there are network level race conditions anyway -MixpanelPeople.prototype._flush = function( - _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback -) { - var _this = this; - - this._flush_one_queue(SET_ACTION, this.set, _set_callback); - this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); - this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); - this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); - this._flush_one_queue(UNION_ACTION, this.union, _union_callback); - - // we have to fire off each $append individually since there is - // no concat method server side - var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); - if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { - var $append_item; - var append_callback = function(response, data) { - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); - } - if (!_.isUndefined(_append_callback)) { - _append_callback(response, data); - } - }; - for (var i = $append_queue.length - 1; i >= 0; i--) { - $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); - $append_item = $append_queue.pop(); - _this._mixpanel['persistence'].save(); - if (!_.isEmptyObject($append_item)) { - _this.append($append_item, append_callback); - } - } - } - - // same for $remove - var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); - if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { - var $remove_item; - var remove_callback = function(response, data) { - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); - } - if (!_.isUndefined(_remove_callback)) { - _remove_callback(response, data); - } - }; - for (var j = $remove_queue.length - 1; j >= 0; j--) { - $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); - $remove_item = $remove_queue.pop(); - _this._mixpanel['persistence'].save(); - if (!_.isEmptyObject($remove_item)) { - _this.remove($remove_item, remove_callback); - } - } - } -}; - -MixpanelPeople.prototype._is_reserved_property = function(prop) { - return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; -}; - -// MixpanelPeople Exports -MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; -MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; -MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; -MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; -MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; -MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; -MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; -MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; -MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; -MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; -MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; - -/* eslint camelcase: "off" */ - -/* - * Constants - */ -/** @const */ var SET_QUEUE_KEY = '__mps'; -/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; -/** @const */ var UNSET_QUEUE_KEY = '__mpus'; -/** @const */ var ADD_QUEUE_KEY = '__mpa'; -/** @const */ var APPEND_QUEUE_KEY = '__mpap'; -/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; -/** @const */ var UNION_QUEUE_KEY = '__mpu'; -// This key is deprecated, but we want to check for it to see whether aliasing is allowed. -/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; -/** @const */ var ALIAS_ID_KEY = '__alias'; -/** @const */ var EVENT_TIMERS_KEY = '__timers'; -/** @const */ var RESERVED_PROPERTIES = [ - SET_QUEUE_KEY, - SET_ONCE_QUEUE_KEY, - UNSET_QUEUE_KEY, - ADD_QUEUE_KEY, - APPEND_QUEUE_KEY, - REMOVE_QUEUE_KEY, - UNION_QUEUE_KEY, - PEOPLE_DISTINCT_ID_KEY, - ALIAS_ID_KEY, - EVENT_TIMERS_KEY -]; - -/** - * Mixpanel Persistence Object - * @constructor - */ -var MixpanelPersistence = function(config) { - this['props'] = {}; - this.campaign_params_saved = false; - - if (config['persistence_name']) { - this.name = 'mp_' + config['persistence_name']; - } else { - this.name = 'mp_' + config['token'] + '_mixpanel'; - } - - var storage_type = config['persistence']; - if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); - storage_type = config['persistence'] = 'cookie'; - } - - if (storage_type === 'localStorage' && _.localStorage.is_supported()) { - this.storage = _.localStorage; - } else { - this.storage = _.cookie; - } - - this.load(); - this.update_config(config); - this.upgrade(); - this.save(); -}; - -MixpanelPersistence.prototype.properties = function() { - var p = {}; - - this.load(); - - // Filter out reserved properties - _.each(this['props'], function(v, k) { - if (!_.include(RESERVED_PROPERTIES, k)) { - p[k] = v; - } - }); - return p; -}; - -MixpanelPersistence.prototype.load = function() { - if (this.disabled) { return; } - - var entry = this.storage.parse(this.name); - - if (entry) { - this['props'] = _.extend({}, entry); - } -}; - -MixpanelPersistence.prototype.upgrade = function() { - var old_cookie, - old_localstorage; - - // if transferring from cookie to localStorage or vice-versa, copy existing - // super properties over to new storage mode - if (this.storage === _.localStorage) { - old_cookie = _.cookie.parse(this.name); - - _.cookie.remove(this.name); - _.cookie.remove(this.name, true); - - if (old_cookie) { - this.register_once(old_cookie); - } - } else if (this.storage === _.cookie) { - old_localstorage = _.localStorage.parse(this.name); - - _.localStorage.remove(this.name); - - if (old_localstorage) { - this.register_once(old_localstorage); - } - } -}; - -MixpanelPersistence.prototype.save = function() { - if (this.disabled) { return; } - - this.storage.set( - this.name, - _.JSONEncode(this['props']), - this.expire_days, - this.cross_subdomain, - this.secure, - this.cross_site, - this.cookie_domain - ); -}; - -MixpanelPersistence.prototype.load_prop = function(key) { - this.load(); - return this['props'][key]; -}; - -MixpanelPersistence.prototype.remove = function() { - // remove both domain and subdomain cookies - this.storage.remove(this.name, false, this.cookie_domain); - this.storage.remove(this.name, true, this.cookie_domain); -}; - -// removes the storage entry and deletes all loaded data -// forced name for tests -MixpanelPersistence.prototype.clear = function() { - this.remove(); - this['props'] = {}; -}; - -/** -* @param {Object} props -* @param {*=} default_value -* @param {number=} days -*/ -MixpanelPersistence.prototype.register_once = function(props, default_value, days) { - if (_.isObject(props)) { - if (typeof(default_value) === 'undefined') { default_value = 'None'; } - this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; - - this.load(); - - _.each(props, function(val, prop) { - if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { - this['props'][prop] = val; - } - }, this); - - this.save(); - - return true; - } - return false; -}; - -/** -* @param {Object} props -* @param {number=} days -*/ -MixpanelPersistence.prototype.register = function(props, days) { - if (_.isObject(props)) { - this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; - - this.load(); - _.extend(this['props'], props); - this.save(); - - return true; - } - return false; -}; - -MixpanelPersistence.prototype.unregister = function(prop) { - this.load(); - if (prop in this['props']) { - delete this['props'][prop]; - this.save(); - } -}; - -MixpanelPersistence.prototype.update_search_keyword = function(referrer) { - this.register(_.info.searchInfo(referrer)); -}; - -// EXPORTED METHOD, we test this directly. -MixpanelPersistence.prototype.update_referrer_info = function(referrer) { - // If referrer doesn't exist, we want to note the fact that it was type-in traffic. - this.register_once({ - '$initial_referrer': referrer || '$direct', - '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' - }, ''); -}; - -MixpanelPersistence.prototype.get_referrer_info = function() { - return _.strip_empty_properties({ - '$initial_referrer': this['props']['$initial_referrer'], - '$initial_referring_domain': this['props']['$initial_referring_domain'] - }); -}; - -MixpanelPersistence.prototype.update_config = function(config) { - this.default_expiry = this.expire_days = config['cookie_expiration']; - this.set_disabled(config['disable_persistence']); - this.set_cookie_domain(config['cookie_domain']); - this.set_cross_site(config['cross_site_cookie']); - this.set_cross_subdomain(config['cross_subdomain_cookie']); - this.set_secure(config['secure_cookie']); -}; - -MixpanelPersistence.prototype.set_disabled = function(disabled) { - this.disabled = disabled; - if (this.disabled) { - this.remove(); - } else { - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { - if (cookie_domain !== this.cookie_domain) { - this.remove(); - this.cookie_domain = cookie_domain; - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cross_site = function(cross_site) { - if (cross_site !== this.cross_site) { - this.cross_site = cross_site; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { - if (cross_subdomain !== this.cross_subdomain) { - this.cross_subdomain = cross_subdomain; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype.get_cross_subdomain = function() { - return this.cross_subdomain; -}; - -MixpanelPersistence.prototype.set_secure = function(secure) { - if (secure !== this.secure) { - this.secure = secure ? true : false; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { - var q_key = this._get_queue_key(queue), - q_data = data[queue], - set_q = this._get_or_create_queue(SET_ACTION), - set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), - unset_q = this._get_or_create_queue(UNSET_ACTION), - add_q = this._get_or_create_queue(ADD_ACTION), - union_q = this._get_or_create_queue(UNION_ACTION), - remove_q = this._get_or_create_queue(REMOVE_ACTION, []), - append_q = this._get_or_create_queue(APPEND_ACTION, []); - - if (q_key === SET_QUEUE_KEY) { - // Update the set queue - we can override any existing values - _.extend(set_q, q_data); - // if there was a pending increment, override it - // with the set. - this._pop_from_people_queue(ADD_ACTION, q_data); - // if there was a pending union, override it - // with the set. - this._pop_from_people_queue(UNION_ACTION, q_data); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === SET_ONCE_QUEUE_KEY) { - // only queue the data if there is not already a set_once call for it. - _.each(q_data, function(v, k) { - if (!(k in set_once_q)) { - set_once_q[k] = v; - } - }); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === UNSET_QUEUE_KEY) { - _.each(q_data, function(prop) { - - // undo previously-queued actions on this key - _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { - if (prop in enqueued_obj) { - delete enqueued_obj[prop]; - } - }); - _.each(append_q, function(append_obj) { - if (prop in append_obj) { - delete append_obj[prop]; - } - }); - - unset_q[prop] = true; - - }); - } else if (q_key === ADD_QUEUE_KEY) { - _.each(q_data, function(v, k) { - // If it exists in the set queue, increment - // the value - if (k in set_q) { - set_q[k] += v; - } else { - // If it doesn't exist, update the add - // queue - if (!(k in add_q)) { - add_q[k] = 0; - } - add_q[k] += v; - } - }, this); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === UNION_QUEUE_KEY) { - _.each(q_data, function(v, k) { - if (_.isArray(v)) { - if (!(k in union_q)) { - union_q[k] = []; - } - // We may send duplicates, the server will dedup them. - union_q[k] = union_q[k].concat(v); - } - }); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === REMOVE_QUEUE_KEY) { - remove_q.push(q_data); - this._pop_from_people_queue(APPEND_ACTION, q_data); - } else if (q_key === APPEND_QUEUE_KEY) { - append_q.push(q_data); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } - - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); - - this.save(); -}; - -MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { - var q = this['props'][this._get_queue_key(queue)]; - if (!_.isUndefined(q)) { - _.each(data, function(v, k) { - if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { - // list actions: only remove if both k+v match - // e.g. remove should not override append in a case like - // append({foo: 'bar'}); remove({foo: 'qux'}) - _.each(q, function(queued_action) { - if (queued_action[k] === v) { - delete queued_action[k]; - } - }); - } else { - delete q[k]; - } - }, this); - } -}; - -MixpanelPersistence.prototype.load_queue = function(queue) { - return this.load_prop(this._get_queue_key(queue)); -}; - -MixpanelPersistence.prototype._get_queue_key = function(queue) { - if (queue === SET_ACTION) { - return SET_QUEUE_KEY; - } else if (queue === SET_ONCE_ACTION) { - return SET_ONCE_QUEUE_KEY; - } else if (queue === UNSET_ACTION) { - return UNSET_QUEUE_KEY; - } else if (queue === ADD_ACTION) { - return ADD_QUEUE_KEY; - } else if (queue === APPEND_ACTION) { - return APPEND_QUEUE_KEY; - } else if (queue === REMOVE_ACTION) { - return REMOVE_QUEUE_KEY; - } else if (queue === UNION_ACTION) { - return UNION_QUEUE_KEY; - } else { - console.error('Invalid queue:', queue); - } -}; - -MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { - var key = this._get_queue_key(queue); - default_val = _.isUndefined(default_val) ? {} : default_val; - return this['props'][key] || (this['props'][key] = default_val); -}; - -MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { - var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; - timers[event_name] = timestamp; - this['props'][EVENT_TIMERS_KEY] = timers; - this.save(); -}; - -MixpanelPersistence.prototype.remove_event_timer = function(event_name) { - var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; - var timestamp = timers[event_name]; - if (!_.isUndefined(timestamp)) { - delete this['props'][EVENT_TIMERS_KEY][event_name]; - this.save(); - } - return timestamp; -}; - -/* eslint camelcase: "off" */ - -/* - * Mixpanel JS Library - * - * Copyright 2012, Mixpanel, Inc. All Rights Reserved - * http://mixpanel.com/ - * - * Includes portions of Underscore.js - * http://documentcloud.github.com/underscore/ - * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. - * Released under the MIT License. - */ - -// ==ClosureCompiler== -// @compilation_level ADVANCED_OPTIMIZATIONS -// @output_file_name mixpanel-2.8.min.js -// ==/ClosureCompiler== - -/* -SIMPLE STYLE GUIDE: - -this.x === public function -this._x === internal - only use within this file -this.__x === private - only use within the class - -Globals should be all caps -*/ - -var init_type; // MODULE or SNIPPET loader -// allow bundlers to specify how extra code (recorder bundle) should be loaded -// eslint-disable-next-line no-unused-vars -var load_extra_bundle = function(src, _onload) { - throw new Error(src + ' not available in this build.'); -}; - -var mixpanel_master; // main mixpanel instance / object -var INIT_MODULE = 0; -var INIT_SNIPPET = 1; - -var IDENTITY_FUNC = function(x) {return x;}; -var NOOP_FUNC = function() {}; - -/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; -/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; -/** @const */ var PAYLOAD_TYPE_JSON = 'json'; -/** @const */ var DEVICE_ID_PREFIX = '$device:'; - - -/* - * Dynamic... constants? Is that an oxymoron? - */ -// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ -// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials -var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); - -// IE<10 does not support cross-origin XHR's but script tags -// with defer won't block window.onload; ENQUEUE_REQUESTS -// should only be true for Opera<12 -var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); - -// save reference to navigator.sendBeacon so it can be minified -var sendBeacon = null; -if (navigator['sendBeacon']) { - sendBeacon = function() { - // late reference to navigator.sendBeacon to allow patching/spying - return navigator['sendBeacon'].apply(navigator, arguments); - }; -} - -var DEFAULT_API_ROUTES = { - 'track': 'track/', - 'engage': 'engage/', - 'groups': 'groups/', - 'record': 'record/' -}; - -/* - * Module-level globals - */ -var DEFAULT_CONFIG = { - 'api_host': 'https://api-js.mixpanel.com', - 'api_routes': DEFAULT_API_ROUTES, - 'api_method': 'POST', - 'api_transport': 'XHR', - 'api_payload_format': PAYLOAD_TYPE_BASE64, - 'app_host': 'https://mixpanel.com', - 'cdn': 'https://cdn.mxpnl.com', - 'cross_site_cookie': false, - 'cross_subdomain_cookie': true, - 'error_reporter': NOOP_FUNC, - 'persistence': 'cookie', - 'persistence_name': '', - 'cookie_domain': '', - 'cookie_name': '', - 'loaded': NOOP_FUNC, - 'mp_loader': null, - 'track_marketing': true, - 'track_pageview': false, - 'skip_first_touch_marketing': false, - 'store_google': true, - 'stop_utm_persistence': false, - 'save_referrer': true, - 'test': false, - 'verbose': false, - 'img': false, - 'debug': false, - 'track_links_timeout': 300, - 'cookie_expiration': 365, - 'upgrade': false, - 'disable_persistence': false, - 'disable_cookie': false, - 'secure_cookie': false, - 'ip': true, - 'opt_out_tracking_by_default': false, - 'opt_out_persistence_by_default': false, - 'opt_out_tracking_persistence_type': 'localStorage', - 'opt_out_tracking_cookie_prefix': null, - 'property_blacklist': [], - 'xhr_headers': {}, // { header: value, header2: value } - 'ignore_dnt': false, - 'batch_requests': true, - 'batch_size': 50, - 'batch_flush_interval_ms': 5000, - 'batch_request_timeout_ms': 90000, - 'batch_autostart': true, - 'hooks': {}, - 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), - 'record_block_selector': 'img, video', - 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes - 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), - 'record_mask_text_selector': '*', - 'record_max_ms': MAX_RECORDING_MS, - 'record_sessions_percent': 0, - 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' -}; - -var DOM_LOADED = false; - -/** - * Mixpanel Library Object - * @constructor - */ -var MixpanelLib = function() {}; - - -/** - * create_mplib(token:string, config:object, name:string) - * - * This function is used by the init method of MixpanelLib objects - * as well as the main initializer at the end of the JSLib (that - * initializes document.mixpanel as well as any additional instances - * declared before this file has loaded). - */ -var create_mplib = function(token, config, name) { - var instance, - target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; - - if (target && init_type === INIT_MODULE) { - instance = target; - } else { - if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); - return; - } - instance = new MixpanelLib(); - } - - instance._cached_groups = {}; // cache groups in a pool - - instance._init(token, config, name); - - instance['people'] = new MixpanelPeople(); - instance['people']._init(instance); - - if (!instance.get_config('skip_first_touch_marketing')) { - // We need null UTM params in the object because - // UTM parameters act as a tuple. If any UTM param - // is present, then we set all UTM params including - // empty ones together - var utm_params = _.info.campaignParams(null); - var initial_utm_params = {}; - var has_utm = false; - _.each(utm_params, function(utm_value, utm_key) { - initial_utm_params['initial_' + utm_key] = utm_value; - if (utm_value) { - has_utm = true; - } - }); - if (has_utm) { - instance['people'].set_once(initial_utm_params); - } - } - - // if any instance on the page has debug = true, we set the - // global debug to be true - Config.DEBUG = Config.DEBUG || instance.get_config('debug'); - - // if target is not defined, we called init after the lib already - // loaded, so there won't be an array of things to execute - if (!_.isUndefined(target) && _.isArray(target)) { - // Crunch through the people queue first - we queue this data up & - // flush on identify, so it's better to do all these operations first - instance._execute_array.call(instance['people'], target['people']); - instance._execute_array(target); - } - - return instance; -}; - -// Initialization methods - -/** - * This function initializes a new instance of the Mixpanel tracking object. - * All new instances are added to the main mixpanel object as sub properties (such as - * mixpanel.library_name) and also returned by this function. To define a - * second instance on the page, you would call: - * - * mixpanel.init('new token', { your: 'config' }, 'library_name'); - * - * and use it like so: - * - * mixpanel.library_name.track(...); - * - * @param {String} token Your Mixpanel API token - * @param {Object} [config] A dictionary of config options to override. See a list of default config options. - * @param {String} [name] The name for the new mixpanel instance that you want created - */ -MixpanelLib.prototype.init = function (token, config, name) { - if (_.isUndefined(name)) { - this.report_error('You must name your new library: init(token, config, name)'); - return; - } - if (name === PRIMARY_INSTANCE_NAME) { - this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); - return; - } - - var instance = create_mplib(token, config, name); - mixpanel_master[name] = instance; - instance._loaded(); - - return instance; -}; - -// mixpanel._init(token:string, config:object, name:string) -// -// This function sets up the current instance of the mixpanel -// library. The difference between this method and the init(...) -// method is this one initializes the actual instance, whereas the -// init(...) method sets up a new library and calls _init on it. -// -MixpanelLib.prototype._init = function(token, config, name) { - config = config || {}; - - this['__loaded'] = true; - this['config'] = {}; - - var variable_features = {}; - - // default to JSON payload for standard mixpanel.com API hosts - if (!('api_payload_format' in config)) { - var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; - if (api_host.match(/\.mixpanel\.com/)) { - variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; - } - } - - this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { - 'name': name, - 'token': token, - 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' - })); - - this['_jsc'] = NOOP_FUNC; - - this.__dom_loaded_queue = []; - this.__request_queue = []; - this.__disabled_events = []; - this._flags = { - 'disable_all_events': false, - 'identify_called': false - }; - - // set up request queueing/batching - this.request_batchers = {}; - this._batch_requests = this.get_config('batch_requests'); - if (this._batch_requests) { - if (!_.localStorage.is_supported(true) || !USE_XHR) { - this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); - _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); - _.localStorage.remove(batcher_config.queue_key); - }); - } else { - this.init_batchers(); - if (sendBeacon && win.addEventListener) { - // Before page closes or hides (user tabs away etc), attempt to flush any events - // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, - // events will not be removed from the persistent store; if the site is loaded again, - // the events will be flushed again on startup and deduplicated on the Mixpanel server - // side. - // There is no reliable way to capture only page close events, so we lean on the - // visibilitychange and pagehide events as recommended at - // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. - // These events fire when the user clicks away from the current page/tab, so will occur - // more frequently than page unload, but are the only mechanism currently for capturing - // this scenario somewhat reliably. - var flush_on_unload = _.bind(function() { - if (!this.request_batchers.events.stopped) { - this.request_batchers.events.flush({unloading: true}); - } - }, this); - win.addEventListener('pagehide', function(ev) { - if (ev['persisted']) { - flush_on_unload(); - } - }); - win.addEventListener('visibilitychange', function() { - if (document$1['visibilityState'] === 'hidden') { - flush_on_unload(); - } - }); - } - } - } - - this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); - this.unpersisted_superprops = {}; - this._gdpr_init(); - - var uuid = _.UUID(); - if (!this.get_distinct_id()) { - // There is no need to set the distinct id - // or the device id if something was already stored - // in the persitence - this.register_once({ - 'distinct_id': DEVICE_ID_PREFIX + uuid, - '$device_id': uuid - }, ''); - } - - var track_pageview_option = this.get_config('track_pageview'); - if (track_pageview_option) { - this._init_url_change_tracking(track_pageview_option); - } - - if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { - this.start_session_recording(); - } -}; - -MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { - if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); - return; - } - - var handleLoadedRecorder = _.bind(function() { - this._recorder = this._recorder || new win['__mp_recorder'](this); - this._recorder['startRecording'](); - }, this); - - if (_.isUndefined(win['__mp_recorder'])) { - load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); - } else { - handleLoadedRecorder(); - } -}); - -MixpanelLib.prototype.stop_session_recording = function () { - if (this._recorder) { - this._recorder['stopRecording'](); - } else { - console.critical('Session recorder module not loaded'); - } -}; - -MixpanelLib.prototype.get_session_recording_properties = function () { - var props = {}; - if (this._recorder) { - var replay_id = this._recorder['replayId']; - if (replay_id) { - props['$mp_replay_id'] = replay_id; - } - } - return props; -}; - -// Private methods - -MixpanelLib.prototype._loaded = function() { - this.get_config('loaded')(this); - this._set_default_superprops(); - this['people'].set_once(this['persistence'].get_referrer_info()); - - // `store_google` is now deprecated and previously stored UTM parameters are cleared - // from persistence by default. - if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { - var utm_params = _.info.campaignParams(null); - _.each(utm_params, function(_utm_value, utm_key) { - // We need to unregister persisted UTM parameters so old values - // are not mixed with the new UTM parameters - this.unregister(utm_key); - }.bind(this)); - } -}; - -// update persistence with info on referrer, UTM params, etc -MixpanelLib.prototype._set_default_superprops = function() { - this['persistence'].update_search_keyword(document$1.referrer); - // Registering super properties for UTM persistence by 'store_google' is deprecated. - if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { - this.register(_.info.campaignParams()); - } - if (this.get_config('save_referrer')) { - this['persistence'].update_referrer_info(document$1.referrer); - } -}; - -MixpanelLib.prototype._dom_loaded = function() { - _.each(this.__dom_loaded_queue, function(item) { - this._track_dom.apply(this, item); - }, this); - - if (!this.has_opted_out_tracking()) { - _.each(this.__request_queue, function(item) { - this._send_request.apply(this, item); - }, this); - } - - delete this.__dom_loaded_queue; - delete this.__request_queue; -}; - -MixpanelLib.prototype._track_dom = function(DomClass, args) { - if (this.get_config('img')) { - this.report_error('You can\'t use DOM tracking functions with img = true.'); - return false; - } - - if (!DOM_LOADED) { - this.__dom_loaded_queue.push([DomClass, args]); - return false; - } - - var dt = new DomClass().init(this); - return dt.track.apply(dt, args); -}; - -MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { - var previous_tracked_url = ''; - var tracked = this.track_pageview(); - if (tracked) { - previous_tracked_url = _.info.currentUrl(); - } - - if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { - win.addEventListener('popstate', function() { - win.dispatchEvent(new Event('mp_locationchange')); - }); - win.addEventListener('hashchange', function() { - win.dispatchEvent(new Event('mp_locationchange')); - }); - var nativePushState = win.history.pushState; - if (typeof nativePushState === 'function') { - win.history.pushState = function(state, unused, url) { - nativePushState.call(win.history, state, unused, url); - win.dispatchEvent(new Event('mp_locationchange')); - }; - } - var nativeReplaceState = win.history.replaceState; - if (typeof nativeReplaceState === 'function') { - win.history.replaceState = function(state, unused, url) { - nativeReplaceState.call(win.history, state, unused, url); - win.dispatchEvent(new Event('mp_locationchange')); - }; - } - win.addEventListener('mp_locationchange', function() { - var current_url = _.info.currentUrl(); - var should_track = false; - if (track_pageview_option === 'full-url') { - should_track = current_url !== previous_tracked_url; - } else if (track_pageview_option === 'url-with-path-and-query-string') { - should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; - } else if (track_pageview_option === 'url-with-path') { - should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; - } - - if (should_track) { - var tracked = this.track_pageview(); - if (tracked) { - previous_tracked_url = current_url; - } - } - }.bind(this)); - } -}; - -/** - * _prepare_callback() should be called by callers of _send_request for use - * as the callback argument. - * - * If there is no callback, this returns null. - * If we are going to make XHR/XDR requests, this returns a function. - * If we are going to use script tags, this returns a string to use as the - * callback GET param. - */ -MixpanelLib.prototype._prepare_callback = function(callback, data) { - if (_.isUndefined(callback)) { - return null; - } - - if (USE_XHR) { - var callback_function = function(response) { - callback(response, data); - }; - return callback_function; - } else { - // if the user gives us a callback, we store as a random - // property on this instances jsc function and update our - // callback string to reflect that. - var jsc = this['_jsc']; - var randomized_cb = '' + Math.floor(Math.random() * 100000000); - var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; - jsc[randomized_cb] = function(response) { - delete jsc[randomized_cb]; - callback(response, data); - }; - return callback_string; - } -}; - -MixpanelLib.prototype._send_request = function(url, data, options, callback) { - var succeeded = true; - - if (ENQUEUE_REQUESTS) { - this.__request_queue.push(arguments); - return succeeded; - } - - var DEFAULT_OPTIONS = { - method: this.get_config('api_method'), - transport: this.get_config('api_transport'), - verbose: this.get_config('verbose') - }; - var body_data = null; - - if (!callback && (_.isFunction(options) || typeof options === 'string')) { - callback = options; - options = null; - } - options = _.extend(DEFAULT_OPTIONS, options || {}); - if (!USE_XHR) { - options.method = 'GET'; - } - var use_post = options.method === 'POST'; - var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; - - // needed to correctly format responses - var verbose_mode = options.verbose; - if (data['verbose']) { verbose_mode = true; } - - if (this.get_config('test')) { data['test'] = 1; } - if (verbose_mode) { data['verbose'] = 1; } - if (this.get_config('img')) { data['img'] = 1; } - if (!USE_XHR) { - if (callback) { - data['callback'] = callback; - } else if (verbose_mode || this.get_config('test')) { - // Verbose output (from verbose mode, or an error in test mode) is a json blob, - // which by itself is not valid javascript. Without a callback, this verbose output will - // cause an error when returned via jsonp, so we force a no-op callback param. - // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 - data['callback'] = '(function(){})'; - } - } - - data['ip'] = this.get_config('ip')?1:0; - data['_'] = new Date().getTime().toString(); - - if (use_post) { - body_data = 'data=' + encodeURIComponent(data['data']); - delete data['data']; - } - - url += '?' + _.HTTPBuildQuery(data); - - var lib = this; - if ('img' in data) { - var img = document$1.createElement('img'); - img.src = url; - document$1.body.appendChild(img); - } else if (use_sendBeacon) { - try { - succeeded = sendBeacon(url, body_data); - } catch (e) { - lib.report_error(e); - succeeded = false; - } - try { - if (callback) { - callback(succeeded ? 1 : 0); - } - } catch (e) { - lib.report_error(e); - } - } else if (USE_XHR) { - try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - - var headers = this.get_config('xhr_headers'); - if (use_post) { - headers['Content-Type'] = 'application/x-www-form-urlencoded'; - } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); - } catch (e) { - lib.report_error(e); - succeeded = false; - } - } else { - var script = document$1.createElement('script'); - script.type = 'text/javascript'; - script.async = true; - script.defer = true; - script.src = url; - var s = document$1.getElementsByTagName('script')[0]; - s.parentNode.insertBefore(script, s); - } - - return succeeded; -}; - -/** - * _execute_array() deals with processing any mixpanel function - * calls that were called before the Mixpanel library were loaded - * (and are thus stored in an array so they can be called later) - * - * Note: we fire off all the mixpanel function calls && user defined - * functions BEFORE we fire off mixpanel tracking calls. This is so - * identify/register/set_config calls can properly modify early - * tracking calls. - * - * @param {Array} array - */ -MixpanelLib.prototype._execute_array = function(array) { - var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; - _.each(array, function(item) { - if (item) { - fn_name = item[0]; - if (_.isArray(fn_name)) { - tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() - } else if (typeof(item) === 'function') { - item.call(this); - } else if (_.isArray(item) && fn_name === 'alias') { - alias_calls.push(item); - } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { - tracking_calls.push(item); - } else { - other_calls.push(item); - } - } - }, this); - - var execute = function(calls, context) { - _.each(calls, function(item) { - if (_.isArray(item[0])) { - // chained call - var caller = context; - _.each(item, function(call) { - caller = caller[call[0]].apply(caller, call.slice(1)); - }); - } else { - this[item[0]].apply(this, item.slice(1)); - } - }, context); - }; - - execute(alias_calls, this); - execute(other_calls, this); - execute(tracking_calls, this); -}; - -// request queueing utils - -MixpanelLib.prototype.are_batchers_initialized = function() { - return !!this.request_batchers.events; -}; - -MixpanelLib.prototype.get_batcher_configs = function() { - var queue_prefix = '__mpq_' + this.get_config('token'); - var api_routes = this.get_config('api_routes'); - this._batcher_configs = this._batcher_configs || { - events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, - people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, - groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} - }; - return this._batcher_configs; -}; - -MixpanelLib.prototype.init_batchers = function() { - if (!this.are_batchers_initialized()) { - var batcher_for = _.bind(function(attrs) { - return new RequestBatcher( - attrs.queue_key, - { - libConfig: this['config'], - sendRequestFunc: _.bind(function(data, options, cb) { - this._send_request( - this.get_config('api_host') + attrs.endpoint, - this._encode_data_for_request(data), - options, - this._prepare_callback(cb, data) - ); - }, this), - beforeSendHook: _.bind(function(item) { - return this._run_hook('before_send_' + attrs.type, item); - }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) - } - ); - }, this); - var batcher_configs = this.get_batcher_configs(); - this.request_batchers = { - events: batcher_for(batcher_configs.events), - people: batcher_for(batcher_configs.people), - groups: batcher_for(batcher_configs.groups) - }; - } - if (this.get_config('batch_autostart')) { - this.start_batch_senders(); - } -}; - -MixpanelLib.prototype.start_batch_senders = function() { - this._batchers_were_started = true; - if (this.are_batchers_initialized()) { - this._batch_requests = true; - _.each(this.request_batchers, function(batcher) { - batcher.start(); - }); - } -}; - -MixpanelLib.prototype.stop_batch_senders = function() { - this._batch_requests = false; - _.each(this.request_batchers, function(batcher) { - batcher.stop(); - batcher.clear(); - }); -}; - -/** - * push() keeps the standard async-array-push - * behavior around after the lib is loaded. - * This is only useful for external integrations that - * do not wish to rely on our convenience methods - * (created in the snippet). - * - * ### Usage: - * mixpanel.push(['register', { a: 'b' }]); - * - * @param {Array} item A [function_name, args...] array to be executed - */ -MixpanelLib.prototype.push = function(item) { - this._execute_array([item]); -}; - -/** - * Disable events on the Mixpanel object. If passed no arguments, - * this function disables tracking of any event. If passed an - * array of event names, those events will be disabled, but other - * events will continue to be tracked. - * - * Note: this function does not stop other mixpanel functions from - * firing, such as register() or people.set(). - * - * @param {Array} [events] An array of event names to disable - */ -MixpanelLib.prototype.disable = function(events) { - if (typeof(events) === 'undefined') { - this._flags.disable_all_events = true; - } else { - this.__disabled_events = this.__disabled_events.concat(events); - } -}; - -MixpanelLib.prototype._encode_data_for_request = function(data) { - var encoded_data = _.JSONEncode(data); - if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { - encoded_data = _.base64Encode(encoded_data); - } - return {'data': encoded_data}; -}; - -// internal method for handling track vs batch-enqueue logic -MixpanelLib.prototype._track_or_batch = function(options, callback) { - var truncated_data = _.truncate(options.data, 255); - var endpoint = options.endpoint; - var batcher = options.batcher; - var should_send_immediately = options.should_send_immediately; - var send_request_options = options.send_request_options || {}; - callback = callback || NOOP_FUNC; - - var request_enqueued_or_initiated = true; - var send_request_immediately = _.bind(function() { - if (!send_request_options.skip_hooks) { - truncated_data = this._run_hook('before_send_' + options.type, truncated_data); - } - if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); - return this._send_request( - endpoint, - this._encode_data_for_request(truncated_data), - send_request_options, - this._prepare_callback(callback, truncated_data) - ); - } else { - return null; - } - }, this); - - if (this._batch_requests && !should_send_immediately) { - batcher.enqueue(truncated_data, function(succeeded) { - if (succeeded) { - callback(1, truncated_data); - } else { - send_request_immediately(); - } - }); - } else { - request_enqueued_or_initiated = send_request_immediately(); - } - - return request_enqueued_or_initiated && truncated_data; -}; - -/** - * Track an event. This is the most important and - * frequently used Mixpanel function. - * - * ### Usage: - * - * // track an event named 'Registered' - * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); - * - * // track an event using navigator.sendBeacon - * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); - * - * To track link clicks or form submissions, see track_links() or track_forms(). - * - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Object} [options] Optional configuration for this track request. - * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). - * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object - * with the tracking payload sent to the API server is returned; otherwise false. - */ -MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { - if (!callback && typeof options === 'function') { - callback = options; - options = null; - } - options = options || {}; - var transport = options['transport']; // external API, don't minify 'transport' prop - if (transport) { - options.transport = transport; // 'transport' prop name can be minified internally - } - var should_send_immediately = options['send_immediately']; - if (typeof callback !== 'function') { - callback = NOOP_FUNC; - } - - if (_.isUndefined(event_name)) { - this.report_error('No event name provided to mixpanel.track'); - return; - } - - if (this._event_is_disabled(event_name)) { - callback(0); - return; - } - - // set defaults - properties = _.extend({}, properties); - properties['token'] = this.get_config('token'); - - // set $duration if time_event was previously called for this event - var start_timestamp = this['persistence'].remove_event_timer(event_name); - if (!_.isUndefined(start_timestamp)) { - var duration_in_ms = new Date().getTime() - start_timestamp; - properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); - } - - this._set_default_superprops(); - - var marketing_properties = this.get_config('track_marketing') - ? _.info.marketingParams() - : {}; - - // note: extend writes to the first object, so lets make sure we - // don't write to the persistence properties object and info - // properties object by passing in a new object - - // update properties with pageview info and super-properties - properties = _.extend( - {}, - _.info.properties({'mp_loader': this.get_config('mp_loader')}), - marketing_properties, - this['persistence'].properties(), - this.unpersisted_superprops, - this.get_session_recording_properties(), - properties - ); - - var property_blacklist = this.get_config('property_blacklist'); - if (_.isArray(property_blacklist)) { - _.each(property_blacklist, function(blacklisted_prop) { - delete properties[blacklisted_prop]; - }); - } else { - this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); - } - - var data = { - 'event': event_name, - 'properties': properties - }; - var ret = this._track_or_batch({ - type: 'events', - data: data, - endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], - batcher: this.request_batchers.events, - should_send_immediately: should_send_immediately, - send_request_options: options - }, callback); - - return ret; -}); - -/** - * Register the current user into one/many groups. - * - * ### Usage: - * - * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs - * mixpanel.set_group('company', 'mixpanel') - * mixpanel.set_group('company', 128746312) - * - * @param {String} group_key Group key - * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - * - */ -MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { - if (!_.isArray(group_ids)) { - group_ids = [group_ids]; - } - var prop = {}; - prop[group_key] = group_ids; - this.register(prop); - return this['people'].set(group_key, group_ids, callback); -}); - -/** - * Add a new group for this user. - * - * ### Usage: - * - * mixpanel.add_group('company', 'mixpanel') - * - * @param {String} group_key Group key - * @param {*} group_id A valid Mixpanel property type - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { - var old_values = this.get_property(group_key); - var prop = {}; - if (old_values === undefined) { - prop[group_key] = [group_id]; - this.register(prop); - } else { - if (old_values.indexOf(group_id) === -1) { - old_values.push(group_id); - prop[group_key] = old_values; - this.register(prop); - } - } - return this['people'].union(group_key, group_id, callback); -}); - -/** - * Remove a group from this user. - * - * ### Usage: - * - * mixpanel.remove_group('company', 'mixpanel') - * - * @param {String} group_key Group key - * @param {*} group_id A valid Mixpanel property type - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { - var old_value = this.get_property(group_key); - // if the value doesn't exist, the persistent store is unchanged - if (old_value !== undefined) { - var idx = old_value.indexOf(group_id); - if (idx > -1) { - old_value.splice(idx, 1); - this.register({group_key: old_value}); - } - if (old_value.length === 0) { - this.unregister(group_key); - } - } - return this['people'].remove(group_key, group_id, callback); -}); - -/** - * Track an event with specific groups. - * - * ### Usage: - * - * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) - * - * @param {String} event_name The name of the event (see `mixpanel.track()`) - * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) - * @param {Object=} groups An object mapping group name keys to one or more values - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { - var tracking_props = _.extend({}, properties || {}); - _.each(groups, function(v, k) { - if (v !== null && v !== undefined) { - tracking_props[k] = v; - } - }); - return this.track(event_name, tracking_props, callback); -}); - -MixpanelLib.prototype._create_map_key = function (group_key, group_id) { - return group_key + '_' + JSON.stringify(group_id); -}; - -MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { - delete this._cached_groups[this._create_map_key(group_key, group_id)]; -}; - -/** - * Look up reference to a Mixpanel group - * - * ### Usage: - * - * mixpanel.get_group(group_key, group_id) - * - * @param {String} group_key Group key - * @param {Object} group_id A valid Mixpanel property type - * @returns {Object} A MixpanelGroup identifier - */ -MixpanelLib.prototype.get_group = function (group_key, group_id) { - var map_key = this._create_map_key(group_key, group_id); - var group = this._cached_groups[map_key]; - if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { - group = new MixpanelGroup(); - group._init(this, group_key, group_id); - this._cached_groups[map_key] = group; - } - return group; -}; - -/** - * Track a default Mixpanel page view event, which includes extra default event properties to - * improve page view data. - * - * ### Usage: - * - * // track a default $mp_web_page_view event - * mixpanel.track_pageview(); - * - * // track a page view event with additional event properties - * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); - * - * // example approach to track page views on different page types as event properties - * mixpanel.track_pageview({'page': 'pricing'}); - * mixpanel.track_pageview({'page': 'homepage'}); - * - * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for - * // individual pages on the same site or product. Use cases for custom event_name may be page - * // views on different products or internal applications that are considered completely separate - * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); - * - * ### Notes: - * - * The `config.track_pageview` option for mixpanel.init() - * may be turned on for tracking page loads automatically. - * - * // track only page loads - * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); - * - * // track when the URL changes in any manner - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); - * - * // track when the URL changes, ignoring any changes in the hash part - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); - * - * // track when the path changes, ignoring any query parameter or hash changes - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); - * - * @param {Object} [properties] An optional set of additional properties to send with the page view event - * @param {Object} [options] Page view tracking options - * @param {String} [options.event_name] - Alternate name for the tracking event - * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object - * with the tracking payload sent to the API server is returned; otherwise false. - */ -MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { - if (typeof properties !== 'object') { - properties = {}; - } - options = options || {}; - var event_name = options['event_name'] || '$mp_web_page_view'; - - var default_page_properties = _.extend( - _.info.mpPageViewProperties(), - _.info.campaignParams(), - _.info.clickParams() - ); - - var event_properties = _.extend( - {}, - default_page_properties, - properties - ); - - return this.track(event_name, event_properties); -}); - -/** - * Track clicks on a set of document elements. Selector must be a - * valid query. Elements must exist on the page at the time track_links is called. - * - * ### Usage: - * - * // track click for link id #nav - * mixpanel.track_links('#nav', 'Clicked Nav Link'); - * - * ### Notes: - * - * This function will wait up to 300 ms for the Mixpanel - * servers to respond. If they have not responded by that time - * it will head to the link without ensuring that your event - * has been tracked. To configure this timeout please see the - * set_config() documentation below. - * - * If you pass a function in as the properties argument, the - * function will receive the DOMElement that triggered the - * event as an argument. You are expected to return an object - * from the function; any properties defined on this object - * will be sent to mixpanel as event properties. - * - * @type {Function} - * @param {Object|String} query A valid DOM query, element or jQuery-esque list - * @param {String} event_name The name of the event to track - * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement - */ -MixpanelLib.prototype.track_links = function() { - return this._track_dom.call(this, LinkTracker, arguments); -}; - -/** - * Track form submissions. Selector must be a valid query. - * - * ### Usage: - * - * // track submission for form id 'register' - * mixpanel.track_forms('#register', 'Created Account'); - * - * ### Notes: - * - * This function will wait up to 300 ms for the mixpanel - * servers to respond, if they have not responded by that time - * it will head to the link without ensuring that your event - * has been tracked. To configure this timeout please see the - * set_config() documentation below. - * - * If you pass a function in as the properties argument, the - * function will receive the DOMElement that triggered the - * event as an argument. You are expected to return an object - * from the function; any properties defined on this object - * will be sent to mixpanel as event properties. - * - * @type {Function} - * @param {Object|String} query A valid DOM query, element or jQuery-esque list - * @param {String} event_name The name of the event to track - * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement - */ -MixpanelLib.prototype.track_forms = function() { - return this._track_dom.call(this, FormTracker, arguments); -}; - -/** - * Time an event by including the time between this call and a - * later 'track' call for the same event in the properties sent - * with the event. - * - * ### Usage: - * - * // time an event named 'Registered' - * mixpanel.time_event('Registered'); - * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); - * - * When called for a particular event name, the next track call for that event - * name will include the elapsed time between the 'time_event' and 'track' - * calls. This value is stored as seconds in the '$duration' property. - * - * @param {String} event_name The name of the event. - */ -MixpanelLib.prototype.time_event = function(event_name) { - if (_.isUndefined(event_name)) { - this.report_error('No event name provided to mixpanel.time_event'); - return; - } - - if (this._event_is_disabled(event_name)) { - return; - } - - this['persistence'].set_event_timer(event_name, new Date().getTime()); -}; - -var REGISTER_DEFAULTS = { - 'persistent': true -}; -/** - * Helper to parse options param for register methods, maintaining - * legacy support for plain "days" param instead of options object - * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods - * @returns {Object} options object - */ -var options_for_register = function(days_or_options) { - var options; - if (_.isObject(days_or_options)) { - options = days_or_options; - } else if (!_.isUndefined(days_or_options)) { - options = {'days': days_or_options}; - } else { - options = {}; - } - return _.extend({}, REGISTER_DEFAULTS, options); -}; - -/** - * Register a set of super properties, which are included with all - * events. This will overwrite previous super property values. - * - * ### Usage: - * - * // register 'Gender' as a super property - * mixpanel.register({'Gender': 'Female'}); - * - * // register several super properties when a user signs up - * mixpanel.register({ - * 'Email': 'jdoe@example.com', - * 'Account Type': 'Free' - * }); - * - * // register only for the current pageload - * mixpanel.register({'Name': 'Pat'}, {persistent: false}); - * - * @param {Object} properties An associative array of properties to store about the user - * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.register = function(props, days_or_options) { - var options = options_for_register(days_or_options); - if (options['persistent']) { - this['persistence'].register(props, options['days']); - } else { - _.extend(this.unpersisted_superprops, props); - } -}; - -/** - * Register a set of super properties only once. This will not - * overwrite previous super property values, unlike register(). - * - * ### Usage: - * - * // register a super property for the first time only - * mixpanel.register_once({ - * 'First Login Date': new Date().toISOString() - * }); - * - * // register once, only for the current pageload - * mixpanel.register_once({ - * 'First interaction time': new Date().toISOString() - * }, 'None', {persistent: false}); - * - * ### Notes: - * - * If default_value is specified, current super properties - * with that value will be overwritten. - * - * @param {Object} properties An associative array of properties to store about the user - * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' - * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { - var options = options_for_register(days_or_options); - if (options['persistent']) { - this['persistence'].register_once(props, default_value, options['days']); - } else { - if (typeof(default_value) === 'undefined') { - default_value = 'None'; - } - _.each(props, function(val, prop) { - if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { - this.unpersisted_superprops[prop] = val; - } - }, this); - } -}; - -/** - * Delete a super property stored with the current user. - * - * @param {String} property The name of the super property to remove - * @param {Object} [options] - * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.unregister = function(property, options) { - options = options_for_register(options); - if (options['persistent']) { - this['persistence'].unregister(property); - } else { - delete this.unpersisted_superprops[property]; - } -}; - -MixpanelLib.prototype._register_single = function(prop, value) { - var props = {}; - props[prop] = value; - this.register(props); -}; - -/** - * Identify a user with a unique ID to track user activity across - * devices, tie a user to their events, and create a user profile. - * If you never call this method, unique visitors are tracked using - * a UUID generated the first time they visit the site. - * - * Call identify when you know the identity of the current user, - * typically after login or signup. We recommend against using - * identify for anonymous visitors to your site. - * - * ### Notes: - * If your project has - * ID Merge - * enabled, the identify method will connect pre- and - * post-authentication events when appropriate. - * - * If your project does not have ID Merge enabled, identify will - * change the user's local distinct_id to the unique ID you pass. - * Events tracked prior to authentication will not be connected - * to the same user identity. If ID Merge is disabled, alias can - * be used to connect pre- and post-registration events. - * - * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. - */ -MixpanelLib.prototype.identify = function( - new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback -) { - // Optional Parameters - // _set_callback:function A callback to be run if and when the People set queue is flushed - // _add_callback:function A callback to be run if and when the People add queue is flushed - // _append_callback:function A callback to be run if and when the People append queue is flushed - // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed - // _union_callback:function A callback to be run if and when the People union queue is flushed - // _unset_callback:function A callback to be run if and when the People unset queue is flushed - - var previous_distinct_id = this.get_distinct_id(); - if (new_distinct_id && previous_distinct_id !== new_distinct_id) { - // we allow the following condition if previous distinct_id is same as new_distinct_id - // so that you can force flush people updates for anonymous profiles. - if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { - this.report_error('distinct_id cannot have $device: prefix'); - return -1; - } - this.register({'$user_id': new_distinct_id}); - } - - if (!this.get_property('$device_id')) { - // The persisted distinct id might not actually be a device id at all - // it might be a distinct id of the user from before - var device_id = previous_distinct_id; - this.register_once({ - '$had_persisted_distinct_id': true, - '$device_id': device_id - }, ''); - } - - // identify only changes the distinct id if it doesn't match either the existing or the alias; - // if it's new, blow away the alias as well. - if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { - this.unregister(ALIAS_ID_KEY); - this.register({'distinct_id': new_distinct_id}); - } - this._flags.identify_called = true; - // Flush any queued up people requests - this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); - - // send an $identify event any time the distinct_id is changing - logic on the server - // will determine whether or not to do anything with it. - if (new_distinct_id !== previous_distinct_id) { - this.track('$identify', { - 'distinct_id': new_distinct_id, - '$anon_distinct_id': previous_distinct_id - }, {skip_hooks: true}); - } -}; - -/** - * Clears super properties and generates a new random distinct_id for this instance. - * Useful for clearing data when a user logs out. - */ -MixpanelLib.prototype.reset = function() { - this['persistence'].clear(); - this._flags.identify_called = false; - var uuid = _.UUID(); - this.register_once({ - 'distinct_id': DEVICE_ID_PREFIX + uuid, - '$device_id': uuid - }, ''); -}; - -/** - * Returns the current distinct id of the user. This is either the id automatically - * generated by the library or the id that has been passed by a call to identify(). - * - * ### Notes: - * - * get_distinct_id() can only be called after the Mixpanel library has finished loading. - * init() has a loaded function available to handle this automatically. For example: - * - * // set distinct_id after the mixpanel library has loaded - * mixpanel.init('YOUR PROJECT TOKEN', { - * loaded: function(mixpanel) { - * distinct_id = mixpanel.get_distinct_id(); - * } - * }); - */ -MixpanelLib.prototype.get_distinct_id = function() { - return this.get_property('distinct_id'); -}; - -/** - * The alias method creates an alias which Mixpanel will use to - * remap one id to another. Multiple aliases can point to the - * same identifier. - * - * The following is a valid use of alias: - * - * mixpanel.alias('new_id', 'existing_id'); - * // You can add multiple id aliases to the existing ID - * mixpanel.alias('newer_id', 'existing_id'); - * - * Aliases can also be chained - the following is a valid example: - * - * mixpanel.alias('new_id', 'existing_id'); - * // chain newer_id - new_id - existing_id - * mixpanel.alias('newer_id', 'new_id'); - * - * Aliases cannot point to multiple identifiers - the following - * example will not work: - * - * mixpanel.alias('new_id', 'existing_id'); - * // this is invalid as 'new_id' already points to 'existing_id' - * mixpanel.alias('new_id', 'newer_id'); - * - * ### Notes: - * - * If your project does not have - * ID Merge - * enabled, the best practice is to call alias once when a unique - * ID is first created for a user (e.g., when a user first registers - * for an account). Do not use alias multiple times for a single - * user without ID Merge enabled. - * - * @param {String} alias A unique identifier that you want to use for this user in the future. - * @param {String} [original] The current identifier being used for this user. - */ -MixpanelLib.prototype.alias = function(alias, original) { - // If the $people_distinct_id key exists in persistence, there has been a previous - // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with - // this ID, as it will duplicate users. - if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { - this.report_error('Attempting to create alias for existing People user - aborting.'); - return -2; - } - - var _this = this; - if (_.isUndefined(original)) { - original = this.get_distinct_id(); - } - if (alias !== original) { - this._register_single(ALIAS_ID_KEY, alias); - return this.track('$create_alias', { - 'alias': alias, - 'distinct_id': original - }, { - skip_hooks: true - }, function() { - // Flush the people queue - _this.identify(alias); - }); - } else { - this.report_error('alias matches current distinct_id - skipping api call.'); - this.identify(alias); - return -1; - } -}; - -/** - * Provide a string to recognize the user by. The string passed to - * this method will appear in the Mixpanel Streams product rather - * than an automatically generated name. Name tags do not have to - * be unique. - * - * This value will only be included in Streams data. - * - * @param {String} name_tag A human readable name for the user - * @deprecated - */ -MixpanelLib.prototype.name_tag = function(name_tag) { - this._register_single('mp_name_tag', name_tag); -}; - -/** - * Update the configuration of a mixpanel library instance. - * - * The default config is: - * - * { - * // host for requests (customizable for e.g. a local proxy) - * api_host: 'https://api-js.mixpanel.com', - * - * // endpoints for different types of requests - * api_routes: { - * track: 'track/', - * engage: 'engage/', - * groups: 'groups/', - * } - * - * // HTTP method for tracking requests - * api_method: 'POST' - * - * // transport for sending requests ('XHR' or 'sendBeacon') - * // NB: sendBeacon should only be used for scenarios such as - * // page unload where a "best-effort" attempt to send is - * // acceptable; the sendBeacon API does not support callbacks - * // or any way to know the result of the request. Mixpanel - * // tracking via sendBeacon will not support any event- - * // batching or retry mechanisms. - * api_transport: 'XHR' - * - * // request-batching/queueing/retry - * batch_requests: true, - * - * // maximum number of events/updates to send in a single - * // network request - * batch_size: 50, - * - * // milliseconds to wait between sending batch requests - * batch_flush_interval_ms: 5000, - * - * // milliseconds to wait for network responses to batch requests - * // before they are considered timed-out and retried - * batch_request_timeout_ms: 90000, - * - * // override value for cookie domain, only useful for ensuring - * // correct cross-subdomain cookies on unusual domains like - * // subdomain.mainsite.avocat.fr; NB this cannot be used to - * // set cookies on a different domain than the current origin - * cookie_domain: '' - * - * // super properties cookie expiration (in days) - * cookie_expiration: 365 - * - * // if true, cookie will be set with SameSite=None; Secure - * // this is only useful in special situations, like embedded - * // 3rd-party iframes that set up a Mixpanel instance - * cross_site_cookie: false - * - * // super properties span subdomains - * cross_subdomain_cookie: true - * - * // debug mode - * debug: false - * - * // if this is true, the mixpanel cookie or localStorage entry - * // will be deleted, and no user persistence will take place - * disable_persistence: false - * - * // if this is true, Mixpanel will automatically determine - * // City, Region and Country data using the IP address of - * //the client - * ip: true - * - * // opt users out of tracking by this Mixpanel instance by default - * opt_out_tracking_by_default: false - * - * // opt users out of browser data storage by this Mixpanel instance by default - * opt_out_persistence_by_default: false - * - * // persistence mechanism used by opt-in/opt-out methods - cookie - * // or localStorage - falls back to cookie if localStorage is unavailable - * opt_out_tracking_persistence_type: 'localStorage' - * - * // customize the name of cookie/localStorage set by opt-in/opt-out methods - * opt_out_tracking_cookie_prefix: null - * - * // type of persistent store for super properties (cookie/ - * // localStorage) if set to 'localStorage', any existing - * // mixpanel cookie value with the same persistence_name - * // will be transferred to localStorage and deleted - * persistence: 'cookie' - * - * // name for super properties persistent store - * persistence_name: '' - * - * // names of properties/superproperties which should never - * // be sent with track() calls - * property_blacklist: [] - * - * // if this is true, mixpanel cookies will be marked as - * // secure, meaning they will only be transmitted over https - * secure_cookie: false - * - * // disables enriching user profiles with first touch marketing data - * skip_first_touch_marketing: false - * - * // the amount of time track_links will - * // wait for Mixpanel's servers to respond - * track_links_timeout: 300 - * - * // adds any UTM parameters and click IDs present on the page to any events fired - * track_marketing: true - * - * // enables automatic page view tracking using default page view events through - * // the track_pageview() method - * track_pageview: false - * - * // if you set upgrade to be true, the library will check for - * // a cookie from our old js library and import super - * // properties from it, then the old cookie is deleted - * // The upgrade config option only works in the initialization, - * // so make sure you set it when you create the library. - * upgrade: false - * - * // extra HTTP request headers to set for each API request, in - * // the format {'Header-Name': value} - * xhr_headers: {} - * - * // whether to ignore or respect the web browser's Do Not Track setting - * ignore_dnt: false - * } - * - * - * @param {Object} config A dictionary of new configuration values to update - */ -MixpanelLib.prototype.set_config = function(config) { - if (_.isObject(config)) { - _.extend(this['config'], config); - - var new_batch_size = config['batch_size']; - if (new_batch_size) { - _.each(this.request_batchers, function(batcher) { - batcher.resetBatchSize(); - }); - } - - if (!this.get_config('persistence_name')) { - this['config']['persistence_name'] = this['config']['cookie_name']; - } - if (!this.get_config('disable_persistence')) { - this['config']['disable_persistence'] = this['config']['disable_cookie']; - } - - if (this['persistence']) { - this['persistence'].update_config(this['config']); - } - Config.DEBUG = Config.DEBUG || this.get_config('debug'); - } -}; - -/** - * returns the current config object for the library. - */ -MixpanelLib.prototype.get_config = function(prop_name) { - return this['config'][prop_name]; -}; - -/** - * Fetch a hook function from config, with safe default, and run it - * against the given arguments - * @param {string} hook_name which hook to retrieve - * @returns {any|null} return value of user-provided hook, or null if nothing was returned - */ -MixpanelLib.prototype._run_hook = function(hook_name) { - var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); - if (typeof ret === 'undefined') { - this.report_error(hook_name + ' hook did not return a value'); - ret = null; - } - return ret; -}; - -/** - * Returns the value of the super property named property_name. If no such - * property is set, get_property() will return the undefined value. - * - * ### Notes: - * - * get_property() can only be called after the Mixpanel library has finished loading. - * init() has a loaded function available to handle this automatically. For example: - * - * // grab value for 'user_id' after the mixpanel library has loaded - * mixpanel.init('YOUR PROJECT TOKEN', { - * loaded: function(mixpanel) { - * user_id = mixpanel.get_property('user_id'); - * } - * }); - * - * @param {String} property_name The name of the super property you want to retrieve - */ -MixpanelLib.prototype.get_property = function(property_name) { - return this['persistence'].load_prop([property_name]); -}; - -MixpanelLib.prototype.toString = function() { - var name = this.get_config('name'); - if (name !== PRIMARY_INSTANCE_NAME) { - name = PRIMARY_INSTANCE_NAME + '.' + name; - } - return name; -}; - -MixpanelLib.prototype._event_is_disabled = function(event_name) { - return _.isBlockedUA(userAgent) || - this._flags.disable_all_events || - _.include(this.__disabled_events, event_name); -}; - -// perform some housekeeping around GDPR opt-in/out state -MixpanelLib.prototype._gdpr_init = function() { - var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; - - // try to convert opt-in/out cookies to localStorage if possible - if (is_localStorage_requested && _.localStorage.is_supported()) { - if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { - this.opt_in_tracking({'enable_persistence': false}); - } - if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { - this.opt_out_tracking({'clear_persistence': false}); - } - this.clear_opt_in_out_tracking({ - 'persistence_type': 'cookie', - 'enable_persistence': false - }); - } - - // check whether the user has already opted out - if so, clear & disable persistence - if (this.has_opted_out_tracking()) { - this._gdpr_update_persistence({'clear_persistence': true}); - - // check whether we should opt out by default - // note: we don't clear persistence here by default since opt-out default state is often - // used as an initial state while GDPR information is being collected - } else if (!this.has_opted_in_tracking() && ( - this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') - )) { - _.cookie.remove('mp_optout'); - this.opt_out_tracking({ - 'clear_persistence': this.get_config('opt_out_persistence_by_default') - }); - } -}; - -/** - * Enable or disable persistence based on options - * only enable/disable if persistence is not already in this state - * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it - * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence - */ -MixpanelLib.prototype._gdpr_update_persistence = function(options) { - var disabled; - if (options && options['clear_persistence']) { - disabled = true; - } else if (options && options['enable_persistence']) { - disabled = false; - } else { - return; - } - - if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { - this['persistence'].set_disabled(disabled); - } - - if (disabled) { - this.stop_batch_senders(); - } else { - // only start batchers after opt-in if they have previously been started - // in order to avoid unintentionally starting up batching for the first time - if (this._batchers_were_started) { - this.start_batch_senders(); - } - } -}; - -// call a base gdpr function after constructing the appropriate token and options args -MixpanelLib.prototype._gdpr_call_func = function(func, options) { - options = _.extend({ - 'track': _.bind(this.track, this), - 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), - 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), - 'cookie_expiration': this.get_config('cookie_expiration'), - 'cross_site_cookie': this.get_config('cross_site_cookie'), - 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), - 'cookie_domain': this.get_config('cookie_domain'), - 'secure_cookie': this.get_config('secure_cookie'), - 'ignore_dnt': this.get_config('ignore_dnt') - }, options); - - // check if localStorage can be used for recording opt out status, fall back to cookie if not - if (!_.localStorage.is_supported()) { - options['persistence_type'] = 'cookie'; - } - - return func(this.get_config('token'), { - track: options['track'], - trackEventName: options['track_event_name'], - trackProperties: options['track_properties'], - persistenceType: options['persistence_type'], - persistencePrefix: options['cookie_prefix'], - cookieDomain: options['cookie_domain'], - cookieExpiration: options['cookie_expiration'], - crossSiteCookie: options['cross_site_cookie'], - crossSubdomainCookie: options['cross_subdomain_cookie'], - secureCookie: options['secure_cookie'], - ignoreDnt: options['ignore_dnt'] - }); -}; - -/** - * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // opt user in - * mixpanel.opt_in_tracking(); - * - * // opt user in with specific event name, properties, cookie configuration - * mixpanel.opt_in_tracking({ - * track_event_name: 'User opted in', - * track_event_properties: { - * 'Email': 'jdoe@example.com' - * }, - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) - * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action - * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action - * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.opt_in_tracking = function(options) { - options = _.extend({ - 'enable_persistence': true - }, options); - - this._gdpr_call_func(optIn, options); - this._gdpr_update_persistence(options); -}; - -/** - * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // opt user out - * mixpanel.opt_out_tracking(); - * - * // opt user out with different cookie configuration from Mixpanel instance - * mixpanel.opt_out_tracking({ - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out - * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.opt_out_tracking = function(options) { - options = _.extend({ - 'clear_persistence': true, - 'delete_user': true - }, options); - - // delete user and clear charges since these methods may be disabled by opt-out - if (options['delete_user'] && this['people'] && this['people']._identify_called()) { - this['people'].delete_user(); - this['people'].clear_charges(); - } - - this._gdpr_call_func(optOut, options); - this._gdpr_update_persistence(options); -}; - -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * var has_opted_in = mixpanel.has_opted_in_tracking(); - * // use has_opted_in value - * - * @param {Object} [options] A dictionary of config options to override - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @returns {boolean} current opt-in status - */ -MixpanelLib.prototype.has_opted_in_tracking = function(options) { - return this._gdpr_call_func(hasOptedIn, options); -}; - -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * var has_opted_out = mixpanel.has_opted_out_tracking(); - * // use has_opted_out value - * - * @param {Object} [options] A dictionary of config options to override - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @returns {boolean} current opt-out status - */ -MixpanelLib.prototype.has_opted_out_tracking = function(options) { - return this._gdpr_call_func(hasOptedOut, options); -}; - -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // clear user's opt-in/out status - * mixpanel.clear_opt_in_out_tracking(); - * - * // clear user's opt-in/out status with specific cookie configuration - should match - * // configuration used when opt_in_tracking/opt_out_tracking methods were called. - * mixpanel.clear_opt_in_out_tracking({ - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { - options = _.extend({ - 'enable_persistence': true - }, options); - - this._gdpr_call_func(clearOptInOut, options); - this._gdpr_update_persistence(options); -}; - -MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); - try { - if (!err && !(msg instanceof Error)) { - msg = new Error(msg); - } - this.get_config('error_reporter')(msg, err); - } catch(err) { - console.error(err); - } -}; - -// EXPORTS (for closure compiler) - -// MixpanelLib Exports -MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; -MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; -MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; -MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; -MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; -MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; -MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; -MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; -MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; -MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; -MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; -MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; -MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; -MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; -MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; -MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; -MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; -MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; -MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; -MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; -MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; -MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; -MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; -MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; -MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; -MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; -MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; -MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; -MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; -MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; -MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; -MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; -MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; -MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; -MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; - -// MixpanelPersistence Exports -MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; -MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; -MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; -MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; -MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; - - -var instances = {}; -var extend_mp = function() { - // add all the sub mixpanel instances - _.each(instances, function(instance, name) { - if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } - }); - - // add private functions as _ - mixpanel_master['_'] = _; -}; - -var override_mp_init_func = function() { - // we override the snippets init function to handle the case where a - // user initializes the mixpanel library after the script loads & runs - mixpanel_master['init'] = function(token, config, name) { - if (name) { - // initialize a sub library - if (!mixpanel_master[name]) { - mixpanel_master[name] = instances[name] = create_mplib(token, config, name); - mixpanel_master[name]._loaded(); - } - return mixpanel_master[name]; - } else { - var instance = mixpanel_master; - - if (instances[PRIMARY_INSTANCE_NAME]) { - // main mixpanel lib already initialized - instance = instances[PRIMARY_INSTANCE_NAME]; - } else if (token) { - // intialize the main mixpanel lib - instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); - instance._loaded(); - instances[PRIMARY_INSTANCE_NAME] = instance; - } - - mixpanel_master = instance; - if (init_type === INIT_SNIPPET) { - win[PRIMARY_INSTANCE_NAME] = mixpanel_master; - } - extend_mp(); - } - }; -}; - -var add_dom_loaded_handler = function() { - // Cross browser DOM Loaded support - function dom_loaded_handler() { - // function flag since we only want to execute this once - if (dom_loaded_handler.done) { return; } - dom_loaded_handler.done = true; - - DOM_LOADED = true; - ENQUEUE_REQUESTS = false; - - _.each(instances, function(inst) { - inst._dom_loaded(); - }); - } - - function do_scroll_check() { - try { - document$1.documentElement.doScroll('left'); - } catch(e) { - setTimeout(do_scroll_check, 1); - return; - } - - dom_loaded_handler(); - } - - if (document$1.addEventListener) { - if (document$1.readyState === 'complete') { - // safari 4 can fire the DOMContentLoaded event before loading all - // external JS (including this file). you will see some copypasta - // on the internet that checks for 'complete' and 'loaded', but - // 'loaded' is an IE thing - dom_loaded_handler(); - } else { - document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); - } - } else if (document$1.attachEvent) { - // IE - document$1.attachEvent('onreadystatechange', dom_loaded_handler); - - // check to make sure we arn't in a frame - var toplevel = false; - try { - toplevel = win.frameElement === null; - } catch(e) { - // noop - } - - if (document$1.documentElement.doScroll && toplevel) { - do_scroll_check(); - } - } - - // fallback handler, always will work - _.register_event(win, 'load', dom_loaded_handler, true); -}; - -function init_as_module(bundle_loader) { - load_extra_bundle = bundle_loader; - init_type = INIT_MODULE; - mixpanel_master = new MixpanelLib(); - - override_mp_init_func(); - mixpanel_master['init'](); - add_dom_loaded_handler(); - - return mixpanel_master; -} - -// For loading separate bundles asynchronously via script tag -// so that we don't load them until they are needed at runtime. -function loadAsync (src, onload) { - var scriptEl = document.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = onload; - scriptEl.src = src; - document.head.appendChild(scriptEl); -} - -/* eslint camelcase: "off" */ - -var mixpanel = init_as_module(loadAsync); - -module.exports = mixpanel; diff --git a/dist/mixpanel-main.cjs.js b/dist/mixpanel-main.cjs.js deleted file mode 100644 index 128f89bd..00000000 --- a/dist/mixpanel-main.cjs.js +++ /dev/null @@ -1,6330 +0,0 @@ -'use strict'; - -var Config = { - DEBUG: false, - LIB_VERSION: '2.53.0' -}; - -/* eslint camelcase: "off", eqeqeq: "off" */ - -// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file -var win; -if (typeof(window) === 'undefined') { - var loc = { - hostname: '' - }; - win = { - navigator: { userAgent: '' }, - document: { - location: loc, - referrer: '' - }, - screen: { width: 0, height: 0 }, - location: loc - }; -} else { - win = window; -} - -// Maximum allowed session recording length -var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours - -/* - * Saved references to long variable names, so that closure compiler can - * minimize file size. - */ - -var ArrayProto = Array.prototype, - FuncProto = Function.prototype, - ObjProto = Object.prototype, - slice = ArrayProto.slice, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty, - windowConsole = win.console, - navigator = win.navigator, - document$1 = win.document, - windowOpera = win.opera, - screen = win.screen, - userAgent = navigator.userAgent; - -var nativeBind = FuncProto.bind, - nativeForEach = ArrayProto.forEach, - nativeIndexOf = ArrayProto.indexOf, - nativeMap = ArrayProto.map, - nativeIsArray = Array.isArray, - breaker = {}; - -var _ = { - trim: function(str) { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill - return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); - } -}; - -// Console override -var console = { - /** @type {function(...*)} */ - log: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - try { - windowConsole.log.apply(windowConsole, arguments); - } catch (err) { - _.each(arguments, function(arg) { - windowConsole.log(arg); - }); - } - } - }, - /** @type {function(...*)} */ - warn: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); - try { - windowConsole.warn.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.warn(arg); - }); - } - } - }, - /** @type {function(...*)} */ - error: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } - }, - /** @type {function(...*)} */ - critical: function() { - if (!_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } - } -}; - -var log_func_with_prefix = function(func, prefix) { - return function() { - arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); - }; -}; -var console_with_prefix = function(prefix) { - return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) - }; -}; - - -// UNDERSCORE -// Embed part of the Underscore Library -_.bind = function(func, context) { - var args, bound; - if (nativeBind && func.bind === nativeBind) { - return nativeBind.apply(func, slice.call(arguments, 1)); - } - if (!_.isFunction(func)) { - throw new TypeError(); - } - args = slice.call(arguments, 2); - bound = function() { - if (!(this instanceof bound)) { - return func.apply(context, args.concat(slice.call(arguments))); - } - var ctor = {}; - ctor.prototype = func.prototype; - var self = new ctor(); - ctor.prototype = null; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) { - return result; - } - return self; - }; - return bound; -}; - -/** - * @param {*=} obj - * @param {function(...*)=} iterator - * @param {Object=} context - */ -_.each = function(obj, iterator, context) { - if (obj === null || obj === undefined) { - return; - } - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { - return; - } - } - } else { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) { - return; - } - } - } - } -}; - -_.extend = function(obj) { - _.each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (source[prop] !== void 0) { - obj[prop] = source[prop]; - } - } - }); - return obj; -}; - -_.isArray = nativeIsArray || function(obj) { - return toString.call(obj) === '[object Array]'; -}; - -// from a comment on http://dbj.org/dbj/?p=286 -// fails on only one very rare and deliberate custom object: -// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; -_.isFunction = function(f) { - try { - return /^\s*\bfunction\b/.test(f); - } catch (x) { - return false; - } -}; - -_.isArguments = function(obj) { - return !!(obj && hasOwnProperty.call(obj, 'callee')); -}; - -_.toArray = function(iterable) { - if (!iterable) { - return []; - } - if (iterable.toArray) { - return iterable.toArray(); - } - if (_.isArray(iterable)) { - return slice.call(iterable); - } - if (_.isArguments(iterable)) { - return slice.call(iterable); - } - return _.values(iterable); -}; - -_.map = function(arr, callback, context) { - if (nativeMap && arr.map === nativeMap) { - return arr.map(callback, context); - } else { - var results = []; - _.each(arr, function(item) { - results.push(callback.call(context, item)); - }); - return results; - } -}; - -_.keys = function(obj) { - var results = []; - if (obj === null) { - return results; - } - _.each(obj, function(value, key) { - results[results.length] = key; - }); - return results; -}; - -_.values = function(obj) { - var results = []; - if (obj === null) { - return results; - } - _.each(obj, function(value) { - results[results.length] = value; - }); - return results; -}; - -_.include = function(obj, target) { - var found = false; - if (obj === null) { - return found; - } - if (nativeIndexOf && obj.indexOf === nativeIndexOf) { - return obj.indexOf(target) != -1; - } - _.each(obj, function(value) { - if (found || (found = (value === target))) { - return breaker; - } - }); - return found; -}; - -_.includes = function(str, needle) { - return str.indexOf(needle) !== -1; -}; - -// Underscore Addons -_.inherit = function(subclass, superclass) { - subclass.prototype = new superclass(); - subclass.prototype.constructor = subclass; - subclass.superclass = superclass.prototype; - return subclass; -}; - -_.isObject = function(obj) { - return (obj === Object(obj) && !_.isArray(obj)); -}; - -_.isEmptyObject = function(obj) { - if (_.isObject(obj)) { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - return false; - } - } - return true; - } - return false; -}; - -_.isUndefined = function(obj) { - return obj === void 0; -}; - -_.isString = function(obj) { - return toString.call(obj) == '[object String]'; -}; - -_.isDate = function(obj) { - return toString.call(obj) == '[object Date]'; -}; - -_.isNumber = function(obj) { - return toString.call(obj) == '[object Number]'; -}; - -_.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); -}; - -_.encodeDates = function(obj) { - _.each(obj, function(v, k) { - if (_.isDate(v)) { - obj[k] = _.formatDate(v); - } else if (_.isObject(v)) { - obj[k] = _.encodeDates(v); // recurse - } - }); - return obj; -}; - -_.timestamp = function() { - Date.now = Date.now || function() { - return +new Date; - }; - return Date.now(); -}; - -_.formatDate = function(d) { - // YYYY-MM-DDTHH:MM:SS in UTC - function pad(n) { - return n < 10 ? '0' + n : n; - } - return d.getUTCFullYear() + '-' + - pad(d.getUTCMonth() + 1) + '-' + - pad(d.getUTCDate()) + 'T' + - pad(d.getUTCHours()) + ':' + - pad(d.getUTCMinutes()) + ':' + - pad(d.getUTCSeconds()); -}; - -_.strip_empty_properties = function(p) { - var ret = {}; - _.each(p, function(v, k) { - if (_.isString(v) && v.length > 0) { - ret[k] = v; - } - }); - return ret; -}; - -/* - * this function returns a copy of object after truncating it. If - * passed an Array or Object it will iterate through obj and - * truncate all the values recursively. - */ -_.truncate = function(obj, length) { - var ret; - - if (typeof(obj) === 'string') { - ret = obj.slice(0, length); - } else if (_.isArray(obj)) { - ret = []; - _.each(obj, function(val) { - ret.push(_.truncate(val, length)); - }); - } else if (_.isObject(obj)) { - ret = {}; - _.each(obj, function(val, key) { - ret[key] = _.truncate(val, length); - }); - } else { - ret = obj; - } - - return ret; -}; - -_.JSONEncode = (function() { - return function(mixed_val) { - var value = mixed_val; - var quote = function(string) { - var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex - var meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"': '\\"', - '\\': '\\\\' - }; - - escapable.lastIndex = 0; - return escapable.test(string) ? - '"' + string.replace(escapable, function(a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : - '"' + string + '"'; - }; - - var str = function(key, holder) { - var gap = ''; - var indent = ' '; - var i = 0; // The loop counter. - var k = ''; // The member key. - var v = ''; // The member value. - var length = 0; - var mind = gap; - var partial = []; - var value = holder[key]; - - // If the value has a toJSON method, call it to obtain a replacement value. - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - - // What happens next depends on the value's type. - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - // JSON numbers must be finite. Encode non-finite numbers as null. - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - // If the value is a boolean or null, convert it to a string. Note: - // typeof null does not produce 'null'. The case is included here in - // the remote chance that this gets fixed someday. - - return String(value); - - case 'object': - // If the type is 'object', we might be dealing with an object or an array or - // null. - // Due to a specification blunder in ECMAScript, typeof null is 'object', - // so watch out for that case. - if (!value) { - return 'null'; - } - - // Make an array to hold the partial results of stringifying this object value. - gap += indent; - partial = []; - - // Is the value an array? - if (toString.apply(value) === '[object Array]') { - // The value is an array. Stringify every element. Use null as a placeholder - // for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - - // Join all of the elements together, separated with commas, and wrap them in - // brackets. - v = partial.length === 0 ? '[]' : - gap ? '[\n' + gap + - partial.join(',\n' + gap) + '\n' + - mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - - // Iterate through all of the keys in the object. - for (k in value) { - if (hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - - // Join all of the member texts together, separated with commas, - // and wrap them in braces. - v = partial.length === 0 ? '{}' : - gap ? '{' + partial.join(',') + '' + - mind + '}' : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - }; - - // Make a fake root object containing our value under the key of ''. - // Return the result of stringifying the value. - return str('', { - '': value - }); - }; -})(); - -/** - * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js - * Slightly modified to throw a real Error rather than a POJO - */ -_.JSONDecode = (function() { - var at, // The index of the current character - ch, // The current character - escapee = { - '"': '"', - '\\': '\\', - '/': '/', - 'b': '\b', - 'f': '\f', - 'n': '\n', - 'r': '\r', - 't': '\t' - }, - text, - error = function(m) { - var e = new SyntaxError(m); - e.at = at; - e.text = text; - throw e; - }, - next = function(c) { - // If a c parameter is provided, verify that it matches the current character. - if (c && c !== ch) { - error('Expected \'' + c + '\' instead of \'' + ch + '\''); - } - // Get the next character. When there are no more characters, - // return the empty string. - ch = text.charAt(at); - at += 1; - return ch; - }, - number = function() { - // Parse a number value. - var number, - string = ''; - - if (ch === '-') { - string = '-'; - next('-'); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - if (ch === '.') { - string += '.'; - while (next() && ch >= '0' && ch <= '9') { - string += ch; - } - } - if (ch === 'e' || ch === 'E') { - string += ch; - next(); - if (ch === '-' || ch === '+') { - string += ch; - next(); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - } - number = +string; - if (!isFinite(number)) { - error('Bad number'); - } else { - return number; - } - }, - - string = function() { - // Parse a string value. - var hex, - i, - string = '', - uffff; - // When parsing for string values, we must look for " and \ characters. - if (ch === '"') { - while (next()) { - if (ch === '"') { - next(); - return string; - } - if (ch === '\\') { - next(); - if (ch === 'u') { - uffff = 0; - for (i = 0; i < 4; i += 1) { - hex = parseInt(next(), 16); - if (!isFinite(hex)) { - break; - } - uffff = uffff * 16 + hex; - } - string += String.fromCharCode(uffff); - } else if (typeof escapee[ch] === 'string') { - string += escapee[ch]; - } else { - break; - } - } else { - string += ch; - } - } - } - error('Bad string'); - }, - white = function() { - // Skip whitespace. - while (ch && ch <= ' ') { - next(); - } - }, - word = function() { - // true, false, or null. - switch (ch) { - case 't': - next('t'); - next('r'); - next('u'); - next('e'); - return true; - case 'f': - next('f'); - next('a'); - next('l'); - next('s'); - next('e'); - return false; - case 'n': - next('n'); - next('u'); - next('l'); - next('l'); - return null; - } - error('Unexpected "' + ch + '"'); - }, - value, // Placeholder for the value function. - array = function() { - // Parse an array value. - var array = []; - - if (ch === '[') { - next('['); - white(); - if (ch === ']') { - next(']'); - return array; // empty array - } - while (ch) { - array.push(value()); - white(); - if (ch === ']') { - next(']'); - return array; - } - next(','); - white(); - } - } - error('Bad array'); - }, - object = function() { - // Parse an object value. - var key, - object = {}; - - if (ch === '{') { - next('{'); - white(); - if (ch === '}') { - next('}'); - return object; // empty object - } - while (ch) { - key = string(); - white(); - next(':'); - if (Object.hasOwnProperty.call(object, key)) { - error('Duplicate key "' + key + '"'); - } - object[key] = value(); - white(); - if (ch === '}') { - next('}'); - return object; - } - next(','); - white(); - } - } - error('Bad object'); - }; - - value = function() { - // Parse a JSON value. It could be an object, an array, a string, - // a number, or a word. - white(); - switch (ch) { - case '{': - return object(); - case '[': - return array(); - case '"': - return string(); - case '-': - return number(); - default: - return ch >= '0' && ch <= '9' ? number() : word(); - } - }; - - // Return the json_parse function. It will have access to all of the - // above functions and variables. - return function(source) { - var result; - - text = source; - at = 0; - ch = ' '; - result = value(); - white(); - if (ch) { - error('Syntax error'); - } - - return result; - }; -})(); - -_.base64Encode = function(data) { - var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, - ac = 0, - enc = '', - tmp_arr = []; - - if (!data) { - return data; - } - - data = _.utf8Encode(data); - - do { // pack three octets into four hexets - o1 = data.charCodeAt(i++); - o2 = data.charCodeAt(i++); - o3 = data.charCodeAt(i++); - - bits = o1 << 16 | o2 << 8 | o3; - - h1 = bits >> 18 & 0x3f; - h2 = bits >> 12 & 0x3f; - h3 = bits >> 6 & 0x3f; - h4 = bits & 0x3f; - - // use hexets to index into b64, and append result to encoded string - tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); - } while (i < data.length); - - enc = tmp_arr.join(''); - - switch (data.length % 3) { - case 1: - enc = enc.slice(0, -2) + '=='; - break; - case 2: - enc = enc.slice(0, -1) + '='; - break; - } - - return enc; -}; - -_.utf8Encode = function(string) { - string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - var utftext = '', - start, - end; - var stringl = 0, - n; - - start = end = 0; - stringl = string.length; - - for (n = 0; n < stringl; n++) { - var c1 = string.charCodeAt(n); - var enc = null; - - if (c1 < 128) { - end++; - } else if ((c1 > 127) && (c1 < 2048)) { - enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); - } else { - enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); - } - if (enc !== null) { - if (end > start) { - utftext += string.substring(start, end); - } - utftext += enc; - start = end = n + 1; - } - } - - if (end > start) { - utftext += string.substring(start, string.length); - } - - return utftext; -}; - -_.UUID = (function() { - - // Time-based entropy - var T = function() { - var time = 1 * new Date(); // cross-browser version of Date.now() - var ticks; - if (win.performance && win.performance.now) { - ticks = win.performance.now(); - } else { - // fall back to busy loop - ticks = 0; - - // this while loop figures how many browser ticks go by - // before 1*new Date() returns a new number, ie the amount - // of ticks that go by per millisecond - while (time == 1 * new Date()) { - ticks++; - } - } - return time.toString(16) + Math.floor(ticks).toString(16); - }; - - // Math.Random entropy - var R = function() { - return Math.random().toString(16).replace('.', ''); - }; - - // User agent entropy - // This function takes the user agent string, and then xors - // together each sequence of 8 bytes. This produces a final - // sequence of 8 bytes which it returns as hex. - var UA = function() { - var ua = userAgent, - i, ch, buffer = [], - ret = 0; - - function xor(result, byte_array) { - var j, tmp = 0; - for (j = 0; j < byte_array.length; j++) { - tmp |= (buffer[j] << j * 8); - } - return result ^ tmp; - } - - for (i = 0; i < ua.length; i++) { - ch = ua.charCodeAt(i); - buffer.unshift(ch & 0xFF); - if (buffer.length >= 4) { - ret = xor(ret, buffer); - buffer = []; - } - } - - if (buffer.length > 0) { - ret = xor(ret, buffer); - } - - return ret.toString(16); - }; - - return function() { - var se = (screen.height * screen.width).toString(16); - return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); - }; -})(); - -// _.isBlockedUA() -// This is to block various web spiders from executing our JS and -// sending false tracking data -var BLOCKED_UA_STRS = [ - 'ahrefsbot', - 'ahrefssiteaudit', - 'baiduspider', - 'bingbot', - 'bingpreview', - 'chrome-lighthouse', - 'facebookexternal', - 'petalbot', - 'pinterest', - 'screaming frog', - 'yahoo! slurp', - 'yandexbot', - - // a whole bunch of goog-specific crawlers - // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers - 'adsbot-google', - 'apis-google', - 'duplexweb-google', - 'feedfetcher-google', - 'google favicon', - 'google web preview', - 'google-read-aloud', - 'googlebot', - 'googleweblight', - 'mediapartners-google', - 'storebot-google' -]; -_.isBlockedUA = function(ua) { - var i; - ua = ua.toLowerCase(); - for (i = 0; i < BLOCKED_UA_STRS.length; i++) { - if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { - return true; - } - } - return false; -}; - -/** - * @param {Object=} formdata - * @param {string=} arg_separator - */ -_.HTTPBuildQuery = function(formdata, arg_separator) { - var use_val, use_key, tmp_arr = []; - - if (_.isUndefined(arg_separator)) { - arg_separator = '&'; - } - - _.each(formdata, function(val, key) { - use_val = encodeURIComponent(val.toString()); - use_key = encodeURIComponent(key); - tmp_arr[tmp_arr.length] = use_key + '=' + use_val; - }); - - return tmp_arr.join(arg_separator); -}; - -_.getQueryParam = function(url, param) { - // Expects a raw URL - - param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); - var regexS = '[\\?&]' + param + '=([^&#]*)', - regex = new RegExp(regexS), - results = regex.exec(url); - if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { - return ''; - } else { - var result = results[1]; - try { - result = decodeURIComponent(result); - } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); - } - return result.replace(/\+/g, ' '); - } -}; - - -// _.cookie -// Methods partially borrowed from quirksmode.org/js/cookies.html -_.cookie = { - get: function(name) { - var nameEQ = name + '='; - var ca = document$1.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1, c.length); - } - if (c.indexOf(nameEQ) === 0) { - return decodeURIComponent(c.substring(nameEQ.length, c.length)); - } - } - return null; - }, - - parse: function(name) { - var cookie; - try { - cookie = _.JSONDecode(_.cookie.get(name)) || {}; - } catch (err) { - // noop - } - return cookie; - }, - - set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { - var cdomain = '', - expires = '', - secure = ''; - - if (domain_override) { - cdomain = '; domain=' + domain_override; - } else if (is_cross_subdomain) { - var domain = extract_domain(document$1.location.hostname); - cdomain = domain ? '; domain=.' + domain : ''; - } - - if (seconds) { - var date = new Date(); - date.setTime(date.getTime() + (seconds * 1000)); - expires = '; expires=' + date.toGMTString(); - } - - if (is_cross_site) { - is_secure = true; - secure = '; SameSite=None'; - } - if (is_secure) { - secure += '; secure'; - } - - document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; - }, - - set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { - var cdomain = '', expires = '', secure = ''; - - if (domain_override) { - cdomain = '; domain=' + domain_override; - } else if (is_cross_subdomain) { - var domain = extract_domain(document$1.location.hostname); - cdomain = domain ? '; domain=.' + domain : ''; - } - - if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = '; expires=' + date.toGMTString(); - } - - if (is_cross_site) { - is_secure = true; - secure = '; SameSite=None'; - } - if (is_secure) { - secure += '; secure'; - } - - var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; - document$1.cookie = new_cookie_val; - return new_cookie_val; - }, - - remove: function(name, is_cross_subdomain, domain_override) { - _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); - } -}; - -var _localStorageSupported = null; -var localStorageSupported = function(storage, forceCheck) { - if (_localStorageSupported !== null && !forceCheck) { - return _localStorageSupported; - } - - var supported = true; - try { - storage = storage || window.localStorage; - var key = '__mplss_' + cheap_guid(8), - val = 'xyz'; - storage.setItem(key, val); - if (storage.getItem(key) !== val) { - supported = false; - } - storage.removeItem(key); - } catch (err) { - supported = false; - } - - _localStorageSupported = supported; - return supported; -}; - -// _.localStorage -_.localStorage = { - is_supported: function(force_check) { - var supported = localStorageSupported(null, force_check); - if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); - } - return supported; - }, - - error: function(msg) { - console.error('localStorage error: ' + msg); - }, - - get: function(name) { - try { - return window.localStorage.getItem(name); - } catch (err) { - _.localStorage.error(err); - } - return null; - }, - - parse: function(name) { - try { - return _.JSONDecode(_.localStorage.get(name)) || {}; - } catch (err) { - // noop - } - return null; - }, - - set: function(name, value) { - try { - window.localStorage.setItem(name, value); - } catch (err) { - _.localStorage.error(err); - } - }, - - remove: function(name) { - try { - window.localStorage.removeItem(name); - } catch (err) { - _.localStorage.error(err); - } - } -}; - -_.register_event = (function() { - // written by Dean Edwards, 2005 - // with input from Tino Zijdel - crisp@xs4all.nl - // with input from Carl Sverre - mail@carlsverre.com - // with input from Mixpanel - // http://dean.edwards.name/weblog/2005/10/add-event/ - // https://gist.github.com/1930440 - - /** - * @param {Object} element - * @param {string} type - * @param {function(...*)} handler - * @param {boolean=} oldSchool - * @param {boolean=} useCapture - */ - var register_event = function(element, type, handler, oldSchool, useCapture) { - if (!element) { - console.error('No valid element provided to register_event'); - return; - } - - if (element.addEventListener && !oldSchool) { - element.addEventListener(type, handler, !!useCapture); - } else { - var ontype = 'on' + type; - var old_handler = element[ontype]; // can be undefined - element[ontype] = makeHandler(element, handler, old_handler); - } - }; - - function makeHandler(element, new_handler, old_handlers) { - var handler = function(event) { - event = event || fixEvent(window.event); - - // this basically happens in firefox whenever another script - // overwrites the onload callback and doesn't pass the event - // object to previously defined callbacks. All the browsers - // that don't define window.event implement addEventListener - // so the dom_loaded handler will still be fired as usual. - if (!event) { - return undefined; - } - - var ret = true; - var old_result, new_result; - - if (_.isFunction(old_handlers)) { - old_result = old_handlers(event); - } - new_result = new_handler.call(element, event); - - if ((false === old_result) || (false === new_result)) { - ret = false; - } - - return ret; - }; - - return handler; - } - - function fixEvent(event) { - if (event) { - event.preventDefault = fixEvent.preventDefault; - event.stopPropagation = fixEvent.stopPropagation; - } - return event; - } - fixEvent.preventDefault = function() { - this.returnValue = false; - }; - fixEvent.stopPropagation = function() { - this.cancelBubble = true; - }; - - return register_event; -})(); - - -var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); - -_.dom_query = (function() { - /* document.getElementsBySelector(selector) - - returns an array of element objects from the current document - matching the CSS selector. Selectors can contain element names, - class names and ids and can be nested. For example: - - elements = document.getElementsBySelector('div#main p a.external') - - Will return an array of all 'a' elements with 'external' in their - class attribute that are contained inside 'p' elements that are - contained inside the 'div' element which has id="main" - - New in version 0.4: Support for CSS2 and CSS3 attribute selectors: - See http://www.w3.org/TR/css3-selectors/#attribute-selectors - - Version 0.4 - Simon Willison, March 25th 2003 - -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows - -- Opera 7 fails - - Version 0.5 - Carl Sverre, Jan 7th 2013 - -- Now uses jQuery-esque `hasClass` for testing class name - equality. This fixes a bug related to '-' characters being - considered not part of a 'word' in regex. - */ - - function getAllChildren(e) { - // Returns all children of element. Workaround required for IE5/Windows. Ugh. - return e.all ? e.all : e.getElementsByTagName('*'); - } - - var bad_whitespace = /[\t\r\n]/g; - - function hasClass(elem, selector) { - var className = ' ' + selector + ' '; - return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); - } - - function getElementsBySelector(selector) { - // Attempt to fail gracefully in lesser browsers - if (!document$1.getElementsByTagName) { - return []; - } - // Split selector in to tokens - var tokens = selector.split(' '); - var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; - var currentContext = [document$1]; - for (i = 0; i < tokens.length; i++) { - token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); - if (token.indexOf('#') > -1) { - // Token is an ID selector - bits = token.split('#'); - tagName = bits[0]; - var id = bits[1]; - var element = document$1.getElementById(id); - if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { - // element not found or tag with that ID not found, return false - return []; - } - // Set currentContext to contain just this element - currentContext = [element]; - continue; // Skip to next token - } - if (token.indexOf('.') > -1) { - // Token contains a class selector - bits = token.split('.'); - tagName = bits[0]; - var className = bits[1]; - if (!tagName) { - tagName = '*'; - } - // Get elements matching tag, filter them for class selector - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - if (tagName == '*') { - elements = getAllChildren(currentContext[j]); - } else { - elements = currentContext[j].getElementsByTagName(tagName); - } - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = []; - currentContextIndex = 0; - for (j = 0; j < found.length; j++) { - if (found[j].className && - _.isString(found[j].className) && // some SVG elements have classNames which are not strings - hasClass(found[j], className) - ) { - currentContext[currentContextIndex++] = found[j]; - } - } - continue; // Skip to next token - } - // Code to deal with attribute selectors - var token_match = token.match(TOKEN_MATCH_REGEX); - if (token_match) { - tagName = token_match[1]; - var attrName = token_match[2]; - var attrOperator = token_match[3]; - var attrValue = token_match[4]; - if (!tagName) { - tagName = '*'; - } - // Grab all of the tagName elements within current context - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - if (tagName == '*') { - elements = getAllChildren(currentContext[j]); - } else { - elements = currentContext[j].getElementsByTagName(tagName); - } - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = []; - currentContextIndex = 0; - var checkFunction; // This function will be used to filter the elements - switch (attrOperator) { - case '=': // Equality - checkFunction = function(e) { - return (e.getAttribute(attrName) == attrValue); - }; - break; - case '~': // Match one of space seperated words - checkFunction = function(e) { - return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); - }; - break; - case '|': // Match start with value followed by optional hyphen - checkFunction = function(e) { - return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); - }; - break; - case '^': // Match starts with value - checkFunction = function(e) { - return (e.getAttribute(attrName).indexOf(attrValue) === 0); - }; - break; - case '$': // Match ends with value - fails with "Warning" in Opera 7 - checkFunction = function(e) { - return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); - }; - break; - case '*': // Match ends with value - checkFunction = function(e) { - return (e.getAttribute(attrName).indexOf(attrValue) > -1); - }; - break; - default: - // Just test for existence of attribute - checkFunction = function(e) { - return e.getAttribute(attrName); - }; - } - currentContext = []; - currentContextIndex = 0; - for (j = 0; j < found.length; j++) { - if (checkFunction(found[j])) { - currentContext[currentContextIndex++] = found[j]; - } - } - // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); - continue; // Skip to next token - } - // If we get here, token is JUST an element (not a class or ID selector) - tagName = token; - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - elements = currentContext[j].getElementsByTagName(tagName); - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = found; - } - return currentContext; - } - - return function(query) { - if (_.isElement(query)) { - return [query]; - } else if (_.isObject(query) && !_.isUndefined(query.length)) { - return query; - } else { - return getElementsBySelector.call(this, query); - } - }; -})(); - -var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; -var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; - -_.info = { - campaignParams: function(default_value) { - var kw = '', - params = {}; - _.each(CAMPAIGN_KEYWORDS, function(kwkey) { - kw = _.getQueryParam(document$1.URL, kwkey); - if (kw.length) { - params[kwkey] = kw; - } else if (default_value !== undefined) { - params[kwkey] = default_value; - } - }); - - return params; - }, - - clickParams: function() { - var id = '', - params = {}; - _.each(CLICK_IDS, function(idkey) { - id = _.getQueryParam(document$1.URL, idkey); - if (id.length) { - params[idkey] = id; - } - }); - - return params; - }, - - marketingParams: function() { - return _.extend(_.info.campaignParams(), _.info.clickParams()); - }, - - searchEngine: function(referrer) { - if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { - return 'google'; - } else if (referrer.search('https?://(.*)bing.com') === 0) { - return 'bing'; - } else if (referrer.search('https?://(.*)yahoo.com') === 0) { - return 'yahoo'; - } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { - return 'duckduckgo'; - } else { - return null; - } - }, - - searchInfo: function(referrer) { - var search = _.info.searchEngine(referrer), - param = (search != 'yahoo') ? 'q' : 'p', - ret = {}; - - if (search !== null) { - ret['$search_engine'] = search; - - var keyword = _.getQueryParam(referrer, param); - if (keyword.length) { - ret['mp_keyword'] = keyword; - } - } - - return ret; - }, - - /** - * This function detects which browser is running this script. - * The order of the checks are important since many user agents - * include key words used in later checks. - */ - browser: function(user_agent, vendor, opera) { - vendor = vendor || ''; // vendor is undefined for at least IE9 - if (opera || _.includes(user_agent, ' OPR/')) { - if (_.includes(user_agent, 'Mini')) { - return 'Opera Mini'; - } - return 'Opera'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { - return 'BlackBerry'; - } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { - return 'Internet Explorer Mobile'; - } else if (_.includes(user_agent, 'SamsungBrowser/')) { - // https://developer.samsung.com/internet/user-agent-string-format - return 'Samsung Internet'; - } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { - return 'Microsoft Edge'; - } else if (_.includes(user_agent, 'FBIOS')) { - return 'Facebook Mobile'; - } else if (_.includes(user_agent, 'Chrome')) { - return 'Chrome'; - } else if (_.includes(user_agent, 'CriOS')) { - return 'Chrome iOS'; - } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { - return 'UC Browser'; - } else if (_.includes(user_agent, 'FxiOS')) { - return 'Firefox iOS'; - } else if (_.includes(vendor, 'Apple')) { - if (_.includes(user_agent, 'Mobile')) { - return 'Mobile Safari'; - } - return 'Safari'; - } else if (_.includes(user_agent, 'Android')) { - return 'Android Mobile'; - } else if (_.includes(user_agent, 'Konqueror')) { - return 'Konqueror'; - } else if (_.includes(user_agent, 'Firefox')) { - return 'Firefox'; - } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { - return 'Internet Explorer'; - } else if (_.includes(user_agent, 'Gecko')) { - return 'Mozilla'; - } else { - return ''; - } - }, - - /** - * This function detects which browser version is running this script, - * parsing major and minor version (e.g., 42.1). User agent strings from: - * http://www.useragentstring.com/pages/useragentstring.php - */ - browserVersion: function(userAgent, vendor, opera) { - var browser = _.info.browser(userAgent, vendor, opera); - var versionRegexs = { - 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, - 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, - 'Chrome': /Chrome\/(\d+(\.\d+)?)/, - 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, - 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, - 'Safari': /Version\/(\d+(\.\d+)?)/, - 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, - 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, - 'Firefox': /Firefox\/(\d+(\.\d+)?)/, - 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, - 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, - 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, - 'Android Mobile': /android\s(\d+(\.\d+)?)/, - 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, - 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, - 'Mozilla': /rv:(\d+(\.\d+)?)/ - }; - var regex = versionRegexs[browser]; - if (regex === undefined) { - return null; - } - var matches = userAgent.match(regex); - if (!matches) { - return null; - } - return parseFloat(matches[matches.length - 2]); - }, - - os: function() { - var a = userAgent; - if (/Windows/i.test(a)) { - if (/Phone/.test(a) || /WPDesktop/.test(a)) { - return 'Windows Phone'; - } - return 'Windows'; - } else if (/(iPhone|iPad|iPod)/.test(a)) { - return 'iOS'; - } else if (/Android/.test(a)) { - return 'Android'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { - return 'BlackBerry'; - } else if (/Mac/i.test(a)) { - return 'Mac OS X'; - } else if (/Linux/.test(a)) { - return 'Linux'; - } else if (/CrOS/.test(a)) { - return 'Chrome OS'; - } else { - return ''; - } - }, - - device: function(user_agent) { - if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { - return 'Windows Phone'; - } else if (/iPad/.test(user_agent)) { - return 'iPad'; - } else if (/iPod/.test(user_agent)) { - return 'iPod Touch'; - } else if (/iPhone/.test(user_agent)) { - return 'iPhone'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { - return 'BlackBerry'; - } else if (/Android/.test(user_agent)) { - return 'Android'; - } else { - return ''; - } - }, - - referringDomain: function(referrer) { - var split = referrer.split('/'); - if (split.length >= 3) { - return split[2]; - } - return ''; - }, - - currentUrl: function() { - return win.location.href; - }, - - properties: function(extra_props) { - if (typeof extra_props !== 'object') { - extra_props = {}; - } - return _.extend(_.strip_empty_properties({ - '$os': _.info.os(), - '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), - '$referrer': document$1.referrer, - '$referring_domain': _.info.referringDomain(document$1.referrer), - '$device': _.info.device(userAgent) - }), { - '$current_url': _.info.currentUrl(), - '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), - '$screen_height': screen.height, - '$screen_width': screen.width, - 'mp_lib': 'web', - '$lib_version': Config.LIB_VERSION, - '$insert_id': cheap_guid(), - 'time': _.timestamp() / 1000 // epoch time in seconds - }, _.strip_empty_properties(extra_props)); - }, - - people_properties: function() { - return _.extend(_.strip_empty_properties({ - '$os': _.info.os(), - '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) - }), { - '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) - }); - }, - - mpPageViewProperties: function() { - return _.strip_empty_properties({ - 'current_page_title': document$1.title, - 'current_domain': win.location.hostname, - 'current_url_path': win.location.pathname, - 'current_url_protocol': win.location.protocol, - 'current_url_search': win.location.search - }); - } -}; - -var cheap_guid = function(maxlen) { - var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); - return maxlen ? guid.substring(0, maxlen) : guid; -}; - -// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) -var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; -// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk -var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; -/** - * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For - * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we - * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). - * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy - * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel - * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date - * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK - * offers the 'cookie_domain' config option to set it explicitly. - * @example - * extract_domain('my.sub.example.com') - * // 'example.com' - */ -var extract_domain = function(hostname) { - var domain_regex = DOMAIN_MATCH_REGEX; - var parts = hostname.split('.'); - var tld = parts[parts.length - 1]; - if (tld.length > 4 || tld === 'com' || tld === 'org') { - domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; - } - var matches = hostname.match(domain_regex); - return matches ? matches[0] : ''; -}; - -var JSONStringify = null, JSONParse = null; -if (typeof JSON !== 'undefined') { - JSONStringify = JSON.stringify; - JSONParse = JSON.parse; -} -JSONStringify = JSONStringify || _.JSONEncode; -JSONParse = JSONParse || _.JSONDecode; - -// EXPORTS (for closure compiler) -_['toArray'] = _.toArray; -_['isObject'] = _.isObject; -_['JSONEncode'] = _.JSONEncode; -_['JSONDecode'] = _.JSONDecode; -_['isBlockedUA'] = _.isBlockedUA; -_['isEmptyObject'] = _.isEmptyObject; -_['info'] = _.info; -_['info']['device'] = _.info.device; -_['info']['browser'] = _.info.browser; -_['info']['browserVersion'] = _.info.browserVersion; -_['info']['properties'] = _.info.properties; - -/* eslint camelcase: "off" */ - -/** - * DomTracker Object - * @constructor - */ -var DomTracker = function() {}; - - -// interface -DomTracker.prototype.create_properties = function() {}; -DomTracker.prototype.event_handler = function() {}; -DomTracker.prototype.after_track_handler = function() {}; - -DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; -}; - -/** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback - */ -DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - - that.event_handler(e, this, options); - - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); - - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); - - return true; -}; - -/** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured - */ -DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; - - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; - - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } - - that.after_track_handler(props, options, timeout_occured); - }; -}; - -DomTracker.prototype.create_properties = function(properties, element) { - var props; - - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } - - return props; -}; - -/** - * LinkTracker Object - * @constructor - * @extends DomTracker - */ -var LinkTracker = function() { - this.override_event = 'click'; -}; -_.inherit(LinkTracker, DomTracker); - -LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); - - if (element.href) { props['url'] = element.href; } - - return props; -}; - -LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; - - if (!options.new_tab) { - evt.preventDefault(); - } -}; - -LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } - - setTimeout(function() { - window.location = options.href; - }, 0); -}; - -/** - * FormTracker Object - * @constructor - * @extends DomTracker - */ -var FormTracker = function() { - this.override_event = 'submit'; -}; -_.inherit(FormTracker, DomTracker); - -FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); -}; - -FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); -}; - -var logger$2 = console_with_prefix('lock'); - -/** - * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser - * window/tab at a time will be able to access shared resources. - * - * Based on the Alur and Taubenfeld fast lock - * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) - * with an added timeout to ensure there will be eventual progress in the event - * that a window is closed in the middle of the callback. - * - * Implementation based on the original version by David Wolever (https://github.com/wolever) - * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. - * - * @example - * const myLock = new SharedLock('some-key'); - * myLock.withLock(function() { - * console.log('I hold the mutex!'); - * }); - * - * @constructor - */ -var SharedLock = function(key, options) { - options = options || {}; - - this.storageKey = key; - this.storage = options.storage || window.localStorage; - this.pollIntervalMS = options.pollIntervalMS || 100; - this.timeoutMS = options.timeoutMS || 2000; -}; - -// pass in a specific pid to test contention scenarios; otherwise -// it is chosen randomly for each acquisition attempt -SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { - if (!pid && typeof errorCB !== 'function') { - pid = errorCB; - errorCB = null; - } - - var i = pid || (new Date().getTime() + '|' + Math.random()); - var startTime = new Date().getTime(); - - var key = this.storageKey; - var pollIntervalMS = this.pollIntervalMS; - var timeoutMS = this.timeoutMS; - var storage = this.storage; - - var keyX = key + ':X'; - var keyY = key + ':Y'; - var keyZ = key + ':Z'; - - var reportError = function(err) { - errorCB && errorCB(err); - }; - - var delay = function(cb) { - if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); - storage.removeItem(keyZ); - storage.removeItem(keyY); - loop(); - return; - } - setTimeout(function() { - try { - cb(); - } catch(err) { - reportError(err); - } - }, pollIntervalMS * (Math.random() + 0.1)); - }; - - var waitFor = function(predicate, cb) { - if (predicate()) { - cb(); - } else { - delay(function() { - waitFor(predicate, cb); - }); - } - }; - - var getSetY = function() { - var valY = storage.getItem(keyY); - if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) - return false; - } else { - storage.setItem(keyY, i); - if (storage.getItem(keyY) === i) { - return true; - } else { - if (!localStorageSupported(storage, true)) { - throw new Error('localStorage support dropped while acquiring lock'); - } - return false; - } - } - }; - - var loop = function() { - storage.setItem(keyX, i); - - waitFor(getSetY, function() { - if (storage.getItem(keyX) === i) { - criticalSection(); - return; - } - - delay(function() { - if (storage.getItem(keyY) !== i) { - loop(); - return; - } - waitFor(function() { - return !storage.getItem(keyZ); - }, criticalSection); - }); - }); - }; - - var criticalSection = function() { - storage.setItem(keyZ, '1'); - try { - lockedCB(); - } finally { - storage.removeItem(keyZ); - if (storage.getItem(keyY) === i) { - storage.removeItem(keyY); - } - if (storage.getItem(keyX) === i) { - storage.removeItem(keyX); - } - } - }; - - try { - if (localStorageSupported(storage, true)) { - loop(); - } else { - throw new Error('localStorage support check failed'); - } - } catch(err) { - reportError(err); - } -}; - -var logger$1 = console_with_prefix('batch'); - -/** - * RequestQueue: queue for batching API requests with localStorage backup for retries. - * Maintains an in-memory queue which represents the source of truth for the current - * page, but also writes all items out to a copy in the browser's localStorage, which - * can be read on subsequent pageloads and retried. For batchability, all the request - * items in the queue should be of the same type (events, people updates, group updates) - * so they can be sent in a single request to the same API endpoint. - * - * LocalStorage keying and locking: In order for reloads and subsequent pageloads of - * the same site to access the same persisted data, they must share the same localStorage - * key (for instance based on project token and queue type). Therefore access to the - * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent - * simultaneously open windows/tabs from overwriting each other's data (which would lead - * to data loss in some situations). - * @constructor - */ -var RequestQueue = function(storageKey, options) { - options = options || {}; - this.storageKey = storageKey; - this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); - this.lock = new SharedLock(storageKey, {storage: this.storage}); - - this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios - - this.memQueue = []; -}; - -/** - * Add one item to queues (memory and localStorage). The queued entry includes - * the given item along with an auto-generated ID and a "flush-after" timestamp. - * It is expected that the item will be sent over the network and dequeued - * before the flush-after time; if this doesn't happen it is considered orphaned - * (e.g., the original tab where it was enqueued got closed before it could be - * sent) and the item can be sent by any tab that finds it in localStorage. - * - * The final callback param is called with a param indicating success or - * failure of the enqueue operation; it is asynchronous because the localStorage - * lock is asynchronous. - */ -RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { - var queueEntry = { - 'id': cheap_guid(), - 'flushAfter': new Date().getTime() + flushInterval * 2, - 'payload': item - }; - - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); -}; - -/** - * Read out the given number of queue entries. If this.memQueue - * has fewer than batchSize items, then look for "orphaned" items - * in the persisted queue (items where the 'flushAfter' time has - * already passed). - */ -RequestQueue.prototype.fillBatch = function(batchSize) { - var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { - // don't need lock just to read events; localStorage is thread-safe - // and the worst that could happen is a duplicate send of some - // orphaned events, which will be deduplicated on the server side - var storedQueue = this.readFromStorage(); - if (storedQueue.length) { - // item IDs already in batch; don't duplicate out of storage - var idsInBatch = {}; // poor man's Set - _.each(batch, function(item) { idsInBatch[item['id']] = true; }); - - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { - item.orphaned = true; - batch.push(item); - if (batch.length >= batchSize) { - break; - } - } - } - } - } - return batch; -}; - -/** - * Remove items with matching 'id' from array (immutably) - * also remove any item without a valid id (e.g., malformed - * storage entries). - */ -var filterOutIDsAndInvalid = function(items, idSet) { - var filteredItems = []; - _.each(items, function(item) { - if (item['id'] && !idSet[item['id']]) { - filteredItems.push(item); - } - }); - return filteredItems; -}; - -/** - * Remove items with matching IDs from both in-memory queue - * and persisted queue - */ -RequestQueue.prototype.removeItemsByID = function(ids, cb) { - var idSet = {}; // poor man's Set - _.each(ids, function(id) { idSet[id] = true; }); - - this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; - } - } - } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); - - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); - } - } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); -}; - -// internal helper for RequestQueue.updatePayloads -var updatePayloads = function(existingItems, itemsToUpdate) { - var newItems = []; - _.each(existingItems, function(item) { - var id = item['id']; - if (id in itemsToUpdate) { - var newPayload = itemsToUpdate[id]; - if (newPayload !== null) { - item['payload'] = newPayload; - newItems.push(item); - } - } else { - // no update - newItems.push(item); - } - }); - return newItems; -}; - -/** - * Update payloads of given items in both in-memory queue and - * persisted queue. Items set to null are removed from queues. - */ -RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { - this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); -}; - -/** - * Read and parse items array from localStorage entry, handling - * malformed/missing data if necessary. - */ -RequestQueue.prototype.readFromStorage = function() { - var storageEntry; - try { - storageEntry = this.storage.getItem(this.storageKey); - if (storageEntry) { - storageEntry = JSONParse(storageEntry); - if (!_.isArray(storageEntry)) { - this.reportError('Invalid storage entry:', storageEntry); - storageEntry = null; - } - } - } catch (err) { - this.reportError('Error retrieving queue', err); - storageEntry = null; - } - return storageEntry || []; -}; - -/** - * Serialize the given items array to localStorage. - */ -RequestQueue.prototype.saveToStorage = function(queue) { - try { - this.storage.setItem(this.storageKey, JSONStringify(queue)); - return true; - } catch (err) { - this.reportError('Error saving queue', err); - return false; - } -}; - -/** - * Clear out queues (memory and localStorage). - */ -RequestQueue.prototype.clear = function() { - this.memQueue = []; - this.storage.removeItem(this.storageKey); -}; - -// maximum interval between request retries after exponential backoff -var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - -var logger = console_with_prefix('batch'); - -/** - * RequestBatcher: manages the queueing, flushing, retry etc of requests of one - * type (events, people, groups). - * Uses RequestQueue to manage the backing store. - * @constructor - */ -var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; - this.queue = new RequestQueue(storageKey, { - errorReporter: _.bind(this.reportError, this), - storage: options.storage - }); - - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; - - this.stopped = !this.libConfig['batch_autostart']; - this.consecutiveRemovalFailures = 0; - - // extra client-side dedupe - this.itemIdsSentSuccessfully = {}; -}; - -/** - * Add one item to queue. - */ -RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); -}; - -/** - * Start flushing batches at the configured time interval. Must call - * this method upon SDK init in order to send anything over the network. - */ -RequestBatcher.prototype.start = function() { - this.stopped = false; - this.consecutiveRemovalFailures = 0; - this.flush(); -}; - -/** - * Stop flushing batches. Can be restarted by calling start(). - */ -RequestBatcher.prototype.stop = function() { - this.stopped = true; - if (this.timeoutID) { - clearTimeout(this.timeoutID); - this.timeoutID = null; - } -}; - -/** - * Clear out queue. - */ -RequestBatcher.prototype.clear = function() { - this.queue.clear(); -}; - -/** - * Restore batch size configuration to whatever is set in the main SDK. - */ -RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; -}; - -/** - * Restore flush interval time configuration to whatever is set in the main SDK. - */ -RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); -}; - -/** - * Schedule the next flush in the given number of milliseconds. - */ -RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; - if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); - } -}; - -/** - * Flush one batch to network. Depending on success/failure modes, it will either - * remove the batch from the queue or leave it in for retry, and schedule the next - * flush. In cases of most network or API failures, it will back off exponentially - * when retrying. - * @param {Object} [options] - * @param {boolean} [options.sendBeacon] - whether to send batch with - * navigator.sendBeacon (only useful for sending batches before page unloads, as - * sendBeacon offers no callbacks or status indications) - */ -RequestBatcher.prototype.flush = function(options) { - try { - - if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); - return; - } - - options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; - var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; - var batch = this.queue.fillBatch(currentBatchSize); - var dataForRequest = []; - var transformedItems = {}; - _.each(batch, function(item) { - var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); - } - if (payload) { - // mp_sent_by_lib_version prop captures which lib version actually - // sends each event (regardless of which version originally queued - // it for sending) - if (payload['event'] && payload['properties']) { - payload['properties'] = _.extend( - {}, - payload['properties'], - {'mp_sent_by_lib_version': Config.LIB_VERSION} - ); - } - var addPayload = true; - var itemId = item['id']; - if (itemId) { - if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { - this.reportError('[dupe] item ID sent too many times, not sending', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - addPayload = false; - } - } else { - this.reportError('[dupe] found item with no ID', {item: item}); - } - - if (addPayload) { - dataForRequest.push(payload); - } - } - transformedItems[item['id']] = payload; - }, this); - if (dataForRequest.length < 1) { - this.resetFlush(); - return; // nothing to do - } - - this.requestInProgress = true; - - var batchSendCallback = _.bind(function(res) { - this.requestInProgress = false; - - try { - - // handle API response in a try-catch to make sure we can reset the - // flush operation if something goes wrong - - var removeItemsFromQueue = false; - if (options.unloading) { - // update persisted data to include hook transformations - this.queue.updatePayloads(transformedItems); - } else if ( - _.isObject(res) && - res.error === 'timeout' && - new Date().getTime() - startTime >= timeoutMS - ) { - this.reportError('Network timeout; retrying'); - this.flush(); - } else if ( - _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') - ) { - // network or API error, or 429 Too Many Requests, retry - var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } - } - retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); - this.reportError('Error; retry in ' + retryMS + ' ms'); - this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { - // 413 Payload Too Large - if (batch.length > 1) { - var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); - this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); - this.reportError('413 response; reducing batch size to ' + this.batchSize); - this.resetFlush(); - } else { - this.reportError('Single-event request too large; dropping', batch); - this.resetBatchSize(); - removeItemsFromQueue = true; - } - } else { - // successful network request+response; remove each item in batch from queue - // (even if it was e.g. a 400, in which case retrying won't help) - removeItemsFromQueue = true; - } - - if (removeItemsFromQueue) { - this.queue.removeItemsByID( - _.map(batch, function(item) { return item['id']; }), - _.bind(function(succeeded) { - if (succeeded) { - this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty - } else { - this.reportError('Failed to remove items from queue'); - if (++this.consecutiveRemovalFailures > 5) { - this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); - } else { - this.resetFlush(); - } - } - }, this) - ); - - // client-side dedupe - _.each(batch, _.bind(function(item) { - var itemId = item['id']; - if (itemId) { - this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; - this.itemIdsSentSuccessfully[itemId]++; - if (this.itemIdsSentSuccessfully[itemId] > 5) { - this.reportError('[dupe] item ID sent too many times', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - } - } else { - this.reportError('[dupe] found item with no ID while removing', {item: item}); - } - }, this)); - } - - } catch(err) { - this.reportError('Error handling API response', err); - this.resetFlush(); - } - }, this); - var requestOptions = { - method: 'POST', - verbose: true, - ignore_json_errors: true, // eslint-disable-line camelcase - timeout_ms: timeoutMS // eslint-disable-line camelcase - }; - if (options.unloading) { - requestOptions.transport = 'sendBeacon'; - } - logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - - } catch(err) { - this.reportError('Error flushing request queue', err); - this.resetFlush(); - } -}; - -/** - * Log error to global logger and optional user-defined logger. - */ -RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); - if (this.errorReporter) { - try { - if (!(err instanceof Error)) { - err = new Error(msg); - } - this.errorReporter(msg, err); - } catch(err) { - logger.error(err); - } - } -}; - -/** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - -/** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ - -/** Public **/ - -var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - -/** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function optIn(token, options) { - _optInOut(true, token, options); -} - -/** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ -function optOut(token, options) { - _optInOut(false, token, options); -} - -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ -function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; -} - -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ -function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; - } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); - } - return optedOut; -} - -/** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); -} - -/** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); - }); -} - -/** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); - }); -} - -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); -} - -/** Private **/ - -/** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ -function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; -} - -/** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ -function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; -} - -/** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type - */ -function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); -} - -/** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ -function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); - - return hasDntOn; -} - -/** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); - return; - } - - options = options || {}; - - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); - - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true - }); - } -} - -/** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; - - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests - - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); - } - - if (!optedOut) { - return method.apply(this, arguments); - } - - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } - - return; - }; -} - -/* eslint camelcase: "off" */ - -/** @const */ var SET_ACTION = '$set'; -/** @const */ var SET_ONCE_ACTION = '$set_once'; -/** @const */ var UNSET_ACTION = '$unset'; -/** @const */ var ADD_ACTION = '$add'; -/** @const */ var APPEND_ACTION = '$append'; -/** @const */ var UNION_ACTION = '$union'; -/** @const */ var REMOVE_ACTION = '$remove'; -/** @const */ var DELETE_ACTION = '$delete'; - -// Common internal methods for mixpanel.people and mixpanel.group APIs. -// These methods shouldn't involve network I/O. -var apiActions = { - set_action: function(prop, to) { - var data = {}; - var $set = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - $set[k] = v; - } - }, this); - } else { - $set[prop] = to; - } - - data[SET_ACTION] = $set; - return data; - }, - - unset_action: function(prop) { - var data = {}; - var $unset = []; - if (!_.isArray(prop)) { - prop = [prop]; - } - - _.each(prop, function(k) { - if (!this._is_reserved_property(k)) { - $unset.push(k); - } - }, this); - - data[UNSET_ACTION] = $unset; - return data; - }, - - set_once_action: function(prop, to) { - var data = {}; - var $set_once = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - $set_once[k] = v; - } - }, this); - } else { - $set_once[prop] = to; - } - data[SET_ONCE_ACTION] = $set_once; - return data; - }, - - union_action: function(list_name, values) { - var data = {}; - var $union = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $union[k] = _.isArray(v) ? v : [v]; - } - }, this); - } else { - $union[list_name] = _.isArray(values) ? values : [values]; - } - data[UNION_ACTION] = $union; - return data; - }, - - append_action: function(list_name, value) { - var data = {}; - var $append = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $append[k] = v; - } - }, this); - } else { - $append[list_name] = value; - } - data[APPEND_ACTION] = $append; - return data; - }, - - remove_action: function(list_name, value) { - var data = {}; - var $remove = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $remove[k] = v; - } - }, this); - } else { - $remove[list_name] = value; - } - data[REMOVE_ACTION] = $remove; - return data; - }, - - delete_action: function() { - var data = {}; - data[DELETE_ACTION] = ''; - return data; - } -}; - -/* eslint camelcase: "off" */ - -/** - * Mixpanel Group Object - * @constructor - */ -var MixpanelGroup = function() {}; - -_.extend(MixpanelGroup.prototype, apiActions); - -MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { - this._mixpanel = mixpanel_instance; - this._group_key = group_key; - this._group_id = group_id; -}; - -/** - * Set properties on a group. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); - * - * // or set multiple properties at once - * mixpanel.get_group('company', 'mixpanel').set({ - * 'Location': '405 Howard', - * 'Founded' : 2009, - * }); - * // properties can be strings, integers, dates, or lists - * - * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. - * @param {*} [to] A value to set on the given property name - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { - var data = this.set_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/** - * Set properties on a group, only if they do not yet exist. - * This will not overwrite previous group property values, unlike - * group.set(). - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); - * - * // or set multiple properties at once - * mixpanel.get_group('company', 'mixpanel').set_once({ - * 'Location': '405 Howard', - * 'Founded' : 2009, - * }); - * // properties can be strings, integers, lists or dates - * - * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. - * @param {*} [to] A value to set on the given property name - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { - var data = this.set_once_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/** - * Unset properties on a group permanently. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').unset('Founded'); - * - * @param {String} prop The name of the property. - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { - var data = this.unset_action(prop); - return this._send_request(data, callback); -}); - -/** - * Merge a given list with a list-valued group property, excluding duplicate values. - * - * ### Usage: - * - * // merge a value to a list, creating it if needed - * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); - * - * @param {String} list_name Name of the property. - * @param {Array} values Values to merge with the given property - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { - if (_.isObject(list_name)) { - callback = values; - } - var data = this.union_action(list_name, values); - return this._send_request(data, callback); -}); - -/** - * Permanently delete a group. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').delete(); - * - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { - // bracket notation above prevents a minification error related to reserved words - var data = this.delete_action(); - return this._send_request(data, callback); -}); - -/** - * Remove a property from a group. The value will be ignored if doesn't exist. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); - * - * @param {String} list_name Name of the property. - * @param {Object} value Value to remove from the given group property - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { - var data = this.remove_action(list_name, value); - return this._send_request(data, callback); -}); - -MixpanelGroup.prototype._send_request = function(data, callback) { - data['$group_key'] = this._group_key; - data['$group_id'] = this._group_id; - data['$token'] = this._get_config('token'); - - var date_encoded_data = _.encodeDates(data); - return this._mixpanel._track_or_batch({ - type: 'groups', - data: date_encoded_data, - endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], - batcher: this._mixpanel.request_batchers.groups - }, callback); -}; - -MixpanelGroup.prototype._is_reserved_property = function(prop) { - return prop === '$group_key' || prop === '$group_id'; -}; - -MixpanelGroup.prototype._get_config = function(conf) { - return this._mixpanel.get_config(conf); -}; - -MixpanelGroup.prototype.toString = function() { - return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; -}; - -// MixpanelGroup Exports -MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; -MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; -MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; -MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; -MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; -MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; - -/* eslint camelcase: "off" */ - -/** - * Mixpanel People Object - * @constructor - */ -var MixpanelPeople = function() {}; - -_.extend(MixpanelPeople.prototype, apiActions); - -MixpanelPeople.prototype._init = function(mixpanel_instance) { - this._mixpanel = mixpanel_instance; -}; - -/* -* Set properties on a user record. -* -* ### Usage: -* -* mixpanel.people.set('gender', 'm'); -* -* // or set multiple properties at once -* mixpanel.people.set({ -* 'Company': 'Acme', -* 'Plan': 'Premium', -* 'Upgrade date': new Date() -* }); -* // properties can be strings, integers, dates, or lists -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [to] A value to set on the given property name -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { - var data = this.set_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - // make sure that the referrer info has been updated and saved - if (this._get_config('save_referrer')) { - this._mixpanel['persistence'].update_referrer_info(document.referrer); - } - - // update $set object with default people properties - data[SET_ACTION] = _.extend( - {}, - _.info.people_properties(), - data[SET_ACTION] - ); - return this._send_request(data, callback); -}); - -/* -* Set properties on a user record, only if they do not yet exist. -* This will not overwrite previous people property values, unlike -* people.set(). -* -* ### Usage: -* -* mixpanel.people.set_once('First Login Date', new Date()); -* -* // or set multiple properties at once -* mixpanel.people.set_once({ -* 'First Login Date': new Date(), -* 'Starting Plan': 'Premium' -* }); -* -* // properties can be strings, integers or dates -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [to] A value to set on the given property name -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { - var data = this.set_once_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/* -* Unset properties on a user record (permanently removes the properties and their values from a profile). -* -* ### Usage: -* -* mixpanel.people.unset('gender'); -* -* // or unset multiple properties at once -* mixpanel.people.unset(['gender', 'Company']); -* -* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { - var data = this.unset_action(prop); - return this._send_request(data, callback); -}); - -/* -* Increment/decrement numeric people analytics properties. -* -* ### Usage: -* -* mixpanel.people.increment('page_views', 1); -* -* // or, for convenience, if you're just incrementing a counter by -* // 1, you can simply do -* mixpanel.people.increment('page_views'); -* -* // to decrement a counter, pass a negative number -* mixpanel.people.increment('credits_left', -1); -* -* // like mixpanel.people.set(), you can increment multiple -* // properties at once: -* mixpanel.people.increment({ -* counter1: 1, -* counter2: 6 -* }); -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. -* @param {Number} [by] An amount to increment the given property -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { - var data = {}; - var $add = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); - return; - } else { - $add[k] = v; - } - } - }, this); - callback = by; - } else { - // convenience: mixpanel.people.increment('property'); will - // increment 'property' by 1 - if (_.isUndefined(by)) { - by = 1; - } - $add[prop] = by; - } - data[ADD_ACTION] = $add; - - return this._send_request(data, callback); -}); - -/* -* Append a value to a list-valued people analytics property. -* -* ### Usage: -* -* // append a value to a list, creating it if needed -* mixpanel.people.append('pages_visited', 'homepage'); -* -* // like mixpanel.people.set(), you can append multiple -* // properties at once: -* mixpanel.people.append({ -* list1: 'bob', -* list2: 123 -* }); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] value An item to append to the list -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { - if (_.isObject(list_name)) { - callback = value; - } - var data = this.append_action(list_name, value); - return this._send_request(data, callback); -}); - -/* -* Remove a value from a list-valued people analytics property. -* -* ### Usage: -* -* mixpanel.people.remove('School', 'UCB'); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] value Item to remove from the list -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { - if (_.isObject(list_name)) { - callback = value; - } - var data = this.remove_action(list_name, value); - return this._send_request(data, callback); -}); - -/* -* Merge a given list with a list-valued people analytics property, -* excluding duplicate values. -* -* ### Usage: -* -* // merge a value to a list, creating it if needed -* mixpanel.people.union('pages_visited', 'homepage'); -* -* // like mixpanel.people.set(), you can append multiple -* // properties at once: -* mixpanel.people.union({ -* list1: 'bob', -* list2: 123 -* }); -* -* // like mixpanel.people.append(), you can append multiple -* // values to the same list: -* mixpanel.people.union({ -* list1: ['bob', 'billy'] -* }); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] Value / values to merge with the given property -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { - if (_.isObject(list_name)) { - callback = values; - } - var data = this.union_action(list_name, values); - return this._send_request(data, callback); -}); - -/* - * Record that you have charged the current user a certain amount - * of money. Charges recorded with track_charge() will appear in the - * Mixpanel revenue report. - * - * ### Usage: - * - * // charge a user $50 - * mixpanel.people.track_charge(50); - * - * // charge a user $30.50 on the 2nd of january - * mixpanel.people.track_charge(30.50, { - * '$time': new Date('jan 1 2012') - * }); - * - * @param {Number} amount The amount of money charged to the current user - * @param {Object} [properties] An associative array of properties associated with the charge - * @param {Function} [callback] If provided, the callback will be called when the server responds - * @deprecated - */ -MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { - if (!_.isNumber(amount)) { - amount = parseFloat(amount); - if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); - return; - } - } - - return this.append('$transactions', _.extend({ - '$amount': amount - }, properties), callback); -}); - -/* - * Permanently clear all revenue report transactions from the - * current user's people analytics profile. - * - * ### Usage: - * - * mixpanel.people.clear_charges(); - * - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - * @deprecated - */ -MixpanelPeople.prototype.clear_charges = function(callback) { - return this.set('$transactions', [], callback); -}; - -/* -* Permanently deletes the current people analytics profile from -* Mixpanel (using the current distinct_id). -* -* ### Usage: -* -* // remove the all data you have stored about the current user -* mixpanel.people.delete_user(); -* -*/ -MixpanelPeople.prototype.delete_user = function() { - if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); - return; - } - var data = {'$delete': this._mixpanel.get_distinct_id()}; - return this._send_request(data); -}; - -MixpanelPeople.prototype.toString = function() { - return this._mixpanel.toString() + '.people'; -}; - -MixpanelPeople.prototype._send_request = function(data, callback) { - data['$token'] = this._get_config('token'); - data['$distinct_id'] = this._mixpanel.get_distinct_id(); - var device_id = this._mixpanel.get_property('$device_id'); - var user_id = this._mixpanel.get_property('$user_id'); - var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); - if (device_id) { - data['$device_id'] = device_id; - } - if (user_id) { - data['$user_id'] = user_id; - } - if (had_persisted_distinct_id) { - data['$had_persisted_distinct_id'] = had_persisted_distinct_id; - } - - var date_encoded_data = _.encodeDates(data); - - if (!this._identify_called()) { - this._enqueue(data); - if (!_.isUndefined(callback)) { - if (this._get_config('verbose')) { - callback({status: -1, error: null}); - } else { - callback(-1); - } - } - return _.truncate(date_encoded_data, 255); - } - - return this._mixpanel._track_or_batch({ - type: 'people', - data: date_encoded_data, - endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], - batcher: this._mixpanel.request_batchers.people - }, callback); -}; - -MixpanelPeople.prototype._get_config = function(conf_var) { - return this._mixpanel.get_config(conf_var); -}; - -MixpanelPeople.prototype._identify_called = function() { - return this._mixpanel._flags.identify_called === true; -}; - -// Queue up engage operations if identify hasn't been called yet. -MixpanelPeople.prototype._enqueue = function(data) { - if (SET_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); - } else if (SET_ONCE_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); - } else if (UNSET_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); - } else if (ADD_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); - } else if (APPEND_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); - } else if (REMOVE_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); - } else if (UNION_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); - } else { - console.error('Invalid call to _enqueue():', data); - } -}; - -MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { - var _this = this; - var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); - var action_params = queued_data; - - if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { - _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); - _this._mixpanel['persistence'].save(); - if (queue_to_params_fn) { - action_params = queue_to_params_fn(queued_data); - } - action_method.call(_this, action_params, function(response, data) { - // on bad response, we want to add it back to the queue - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); - } - if (!_.isUndefined(callback)) { - callback(response, data); - } - }); - } -}; - -// Flush queued engage operations - order does not matter, -// and there are network level race conditions anyway -MixpanelPeople.prototype._flush = function( - _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback -) { - var _this = this; - - this._flush_one_queue(SET_ACTION, this.set, _set_callback); - this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); - this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); - this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); - this._flush_one_queue(UNION_ACTION, this.union, _union_callback); - - // we have to fire off each $append individually since there is - // no concat method server side - var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); - if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { - var $append_item; - var append_callback = function(response, data) { - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); - } - if (!_.isUndefined(_append_callback)) { - _append_callback(response, data); - } - }; - for (var i = $append_queue.length - 1; i >= 0; i--) { - $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); - $append_item = $append_queue.pop(); - _this._mixpanel['persistence'].save(); - if (!_.isEmptyObject($append_item)) { - _this.append($append_item, append_callback); - } - } - } - - // same for $remove - var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); - if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { - var $remove_item; - var remove_callback = function(response, data) { - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); - } - if (!_.isUndefined(_remove_callback)) { - _remove_callback(response, data); - } - }; - for (var j = $remove_queue.length - 1; j >= 0; j--) { - $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); - $remove_item = $remove_queue.pop(); - _this._mixpanel['persistence'].save(); - if (!_.isEmptyObject($remove_item)) { - _this.remove($remove_item, remove_callback); - } - } - } -}; - -MixpanelPeople.prototype._is_reserved_property = function(prop) { - return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; -}; - -// MixpanelPeople Exports -MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; -MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; -MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; -MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; -MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; -MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; -MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; -MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; -MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; -MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; -MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; - -/* eslint camelcase: "off" */ - -/* - * Constants - */ -/** @const */ var SET_QUEUE_KEY = '__mps'; -/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; -/** @const */ var UNSET_QUEUE_KEY = '__mpus'; -/** @const */ var ADD_QUEUE_KEY = '__mpa'; -/** @const */ var APPEND_QUEUE_KEY = '__mpap'; -/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; -/** @const */ var UNION_QUEUE_KEY = '__mpu'; -// This key is deprecated, but we want to check for it to see whether aliasing is allowed. -/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; -/** @const */ var ALIAS_ID_KEY = '__alias'; -/** @const */ var EVENT_TIMERS_KEY = '__timers'; -/** @const */ var RESERVED_PROPERTIES = [ - SET_QUEUE_KEY, - SET_ONCE_QUEUE_KEY, - UNSET_QUEUE_KEY, - ADD_QUEUE_KEY, - APPEND_QUEUE_KEY, - REMOVE_QUEUE_KEY, - UNION_QUEUE_KEY, - PEOPLE_DISTINCT_ID_KEY, - ALIAS_ID_KEY, - EVENT_TIMERS_KEY -]; - -/** - * Mixpanel Persistence Object - * @constructor - */ -var MixpanelPersistence = function(config) { - this['props'] = {}; - this.campaign_params_saved = false; - - if (config['persistence_name']) { - this.name = 'mp_' + config['persistence_name']; - } else { - this.name = 'mp_' + config['token'] + '_mixpanel'; - } - - var storage_type = config['persistence']; - if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); - storage_type = config['persistence'] = 'cookie'; - } - - if (storage_type === 'localStorage' && _.localStorage.is_supported()) { - this.storage = _.localStorage; - } else { - this.storage = _.cookie; - } - - this.load(); - this.update_config(config); - this.upgrade(); - this.save(); -}; - -MixpanelPersistence.prototype.properties = function() { - var p = {}; - - this.load(); - - // Filter out reserved properties - _.each(this['props'], function(v, k) { - if (!_.include(RESERVED_PROPERTIES, k)) { - p[k] = v; - } - }); - return p; -}; - -MixpanelPersistence.prototype.load = function() { - if (this.disabled) { return; } - - var entry = this.storage.parse(this.name); - - if (entry) { - this['props'] = _.extend({}, entry); - } -}; - -MixpanelPersistence.prototype.upgrade = function() { - var old_cookie, - old_localstorage; - - // if transferring from cookie to localStorage or vice-versa, copy existing - // super properties over to new storage mode - if (this.storage === _.localStorage) { - old_cookie = _.cookie.parse(this.name); - - _.cookie.remove(this.name); - _.cookie.remove(this.name, true); - - if (old_cookie) { - this.register_once(old_cookie); - } - } else if (this.storage === _.cookie) { - old_localstorage = _.localStorage.parse(this.name); - - _.localStorage.remove(this.name); - - if (old_localstorage) { - this.register_once(old_localstorage); - } - } -}; - -MixpanelPersistence.prototype.save = function() { - if (this.disabled) { return; } - - this.storage.set( - this.name, - _.JSONEncode(this['props']), - this.expire_days, - this.cross_subdomain, - this.secure, - this.cross_site, - this.cookie_domain - ); -}; - -MixpanelPersistence.prototype.load_prop = function(key) { - this.load(); - return this['props'][key]; -}; - -MixpanelPersistence.prototype.remove = function() { - // remove both domain and subdomain cookies - this.storage.remove(this.name, false, this.cookie_domain); - this.storage.remove(this.name, true, this.cookie_domain); -}; - -// removes the storage entry and deletes all loaded data -// forced name for tests -MixpanelPersistence.prototype.clear = function() { - this.remove(); - this['props'] = {}; -}; - -/** -* @param {Object} props -* @param {*=} default_value -* @param {number=} days -*/ -MixpanelPersistence.prototype.register_once = function(props, default_value, days) { - if (_.isObject(props)) { - if (typeof(default_value) === 'undefined') { default_value = 'None'; } - this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; - - this.load(); - - _.each(props, function(val, prop) { - if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { - this['props'][prop] = val; - } - }, this); - - this.save(); - - return true; - } - return false; -}; - -/** -* @param {Object} props -* @param {number=} days -*/ -MixpanelPersistence.prototype.register = function(props, days) { - if (_.isObject(props)) { - this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; - - this.load(); - _.extend(this['props'], props); - this.save(); - - return true; - } - return false; -}; - -MixpanelPersistence.prototype.unregister = function(prop) { - this.load(); - if (prop in this['props']) { - delete this['props'][prop]; - this.save(); - } -}; - -MixpanelPersistence.prototype.update_search_keyword = function(referrer) { - this.register(_.info.searchInfo(referrer)); -}; - -// EXPORTED METHOD, we test this directly. -MixpanelPersistence.prototype.update_referrer_info = function(referrer) { - // If referrer doesn't exist, we want to note the fact that it was type-in traffic. - this.register_once({ - '$initial_referrer': referrer || '$direct', - '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' - }, ''); -}; - -MixpanelPersistence.prototype.get_referrer_info = function() { - return _.strip_empty_properties({ - '$initial_referrer': this['props']['$initial_referrer'], - '$initial_referring_domain': this['props']['$initial_referring_domain'] - }); -}; - -MixpanelPersistence.prototype.update_config = function(config) { - this.default_expiry = this.expire_days = config['cookie_expiration']; - this.set_disabled(config['disable_persistence']); - this.set_cookie_domain(config['cookie_domain']); - this.set_cross_site(config['cross_site_cookie']); - this.set_cross_subdomain(config['cross_subdomain_cookie']); - this.set_secure(config['secure_cookie']); -}; - -MixpanelPersistence.prototype.set_disabled = function(disabled) { - this.disabled = disabled; - if (this.disabled) { - this.remove(); - } else { - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { - if (cookie_domain !== this.cookie_domain) { - this.remove(); - this.cookie_domain = cookie_domain; - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cross_site = function(cross_site) { - if (cross_site !== this.cross_site) { - this.cross_site = cross_site; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { - if (cross_subdomain !== this.cross_subdomain) { - this.cross_subdomain = cross_subdomain; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype.get_cross_subdomain = function() { - return this.cross_subdomain; -}; - -MixpanelPersistence.prototype.set_secure = function(secure) { - if (secure !== this.secure) { - this.secure = secure ? true : false; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { - var q_key = this._get_queue_key(queue), - q_data = data[queue], - set_q = this._get_or_create_queue(SET_ACTION), - set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), - unset_q = this._get_or_create_queue(UNSET_ACTION), - add_q = this._get_or_create_queue(ADD_ACTION), - union_q = this._get_or_create_queue(UNION_ACTION), - remove_q = this._get_or_create_queue(REMOVE_ACTION, []), - append_q = this._get_or_create_queue(APPEND_ACTION, []); - - if (q_key === SET_QUEUE_KEY) { - // Update the set queue - we can override any existing values - _.extend(set_q, q_data); - // if there was a pending increment, override it - // with the set. - this._pop_from_people_queue(ADD_ACTION, q_data); - // if there was a pending union, override it - // with the set. - this._pop_from_people_queue(UNION_ACTION, q_data); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === SET_ONCE_QUEUE_KEY) { - // only queue the data if there is not already a set_once call for it. - _.each(q_data, function(v, k) { - if (!(k in set_once_q)) { - set_once_q[k] = v; - } - }); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === UNSET_QUEUE_KEY) { - _.each(q_data, function(prop) { - - // undo previously-queued actions on this key - _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { - if (prop in enqueued_obj) { - delete enqueued_obj[prop]; - } - }); - _.each(append_q, function(append_obj) { - if (prop in append_obj) { - delete append_obj[prop]; - } - }); - - unset_q[prop] = true; - - }); - } else if (q_key === ADD_QUEUE_KEY) { - _.each(q_data, function(v, k) { - // If it exists in the set queue, increment - // the value - if (k in set_q) { - set_q[k] += v; - } else { - // If it doesn't exist, update the add - // queue - if (!(k in add_q)) { - add_q[k] = 0; - } - add_q[k] += v; - } - }, this); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === UNION_QUEUE_KEY) { - _.each(q_data, function(v, k) { - if (_.isArray(v)) { - if (!(k in union_q)) { - union_q[k] = []; - } - // We may send duplicates, the server will dedup them. - union_q[k] = union_q[k].concat(v); - } - }); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === REMOVE_QUEUE_KEY) { - remove_q.push(q_data); - this._pop_from_people_queue(APPEND_ACTION, q_data); - } else if (q_key === APPEND_QUEUE_KEY) { - append_q.push(q_data); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } - - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); - - this.save(); -}; - -MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { - var q = this['props'][this._get_queue_key(queue)]; - if (!_.isUndefined(q)) { - _.each(data, function(v, k) { - if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { - // list actions: only remove if both k+v match - // e.g. remove should not override append in a case like - // append({foo: 'bar'}); remove({foo: 'qux'}) - _.each(q, function(queued_action) { - if (queued_action[k] === v) { - delete queued_action[k]; - } - }); - } else { - delete q[k]; - } - }, this); - } -}; - -MixpanelPersistence.prototype.load_queue = function(queue) { - return this.load_prop(this._get_queue_key(queue)); -}; - -MixpanelPersistence.prototype._get_queue_key = function(queue) { - if (queue === SET_ACTION) { - return SET_QUEUE_KEY; - } else if (queue === SET_ONCE_ACTION) { - return SET_ONCE_QUEUE_KEY; - } else if (queue === UNSET_ACTION) { - return UNSET_QUEUE_KEY; - } else if (queue === ADD_ACTION) { - return ADD_QUEUE_KEY; - } else if (queue === APPEND_ACTION) { - return APPEND_QUEUE_KEY; - } else if (queue === REMOVE_ACTION) { - return REMOVE_QUEUE_KEY; - } else if (queue === UNION_ACTION) { - return UNION_QUEUE_KEY; - } else { - console.error('Invalid queue:', queue); - } -}; - -MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { - var key = this._get_queue_key(queue); - default_val = _.isUndefined(default_val) ? {} : default_val; - return this['props'][key] || (this['props'][key] = default_val); -}; - -MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { - var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; - timers[event_name] = timestamp; - this['props'][EVENT_TIMERS_KEY] = timers; - this.save(); -}; - -MixpanelPersistence.prototype.remove_event_timer = function(event_name) { - var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; - var timestamp = timers[event_name]; - if (!_.isUndefined(timestamp)) { - delete this['props'][EVENT_TIMERS_KEY][event_name]; - this.save(); - } - return timestamp; -}; - -/* eslint camelcase: "off" */ - -/* - * Mixpanel JS Library - * - * Copyright 2012, Mixpanel, Inc. All Rights Reserved - * http://mixpanel.com/ - * - * Includes portions of Underscore.js - * http://documentcloud.github.com/underscore/ - * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. - * Released under the MIT License. - */ - -// ==ClosureCompiler== -// @compilation_level ADVANCED_OPTIMIZATIONS -// @output_file_name mixpanel-2.8.min.js -// ==/ClosureCompiler== - -/* -SIMPLE STYLE GUIDE: - -this.x === public function -this._x === internal - only use within this file -this.__x === private - only use within the class - -Globals should be all caps -*/ - -var init_type; // MODULE or SNIPPET loader -// allow bundlers to specify how extra code (recorder bundle) should be loaded -// eslint-disable-next-line no-unused-vars -var load_extra_bundle = function(src, _onload) { - throw new Error(src + ' not available in this build.'); -}; - -var mixpanel_master; // main mixpanel instance / object -var INIT_MODULE = 0; -var INIT_SNIPPET = 1; - -var IDENTITY_FUNC = function(x) {return x;}; -var NOOP_FUNC = function() {}; - -/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; -/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; -/** @const */ var PAYLOAD_TYPE_JSON = 'json'; -/** @const */ var DEVICE_ID_PREFIX = '$device:'; - - -/* - * Dynamic... constants? Is that an oxymoron? - */ -// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ -// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials -var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); - -// IE<10 does not support cross-origin XHR's but script tags -// with defer won't block window.onload; ENQUEUE_REQUESTS -// should only be true for Opera<12 -var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); - -// save reference to navigator.sendBeacon so it can be minified -var sendBeacon = null; -if (navigator['sendBeacon']) { - sendBeacon = function() { - // late reference to navigator.sendBeacon to allow patching/spying - return navigator['sendBeacon'].apply(navigator, arguments); - }; -} - -var DEFAULT_API_ROUTES = { - 'track': 'track/', - 'engage': 'engage/', - 'groups': 'groups/', - 'record': 'record/' -}; - -/* - * Module-level globals - */ -var DEFAULT_CONFIG = { - 'api_host': 'https://api-js.mixpanel.com', - 'api_routes': DEFAULT_API_ROUTES, - 'api_method': 'POST', - 'api_transport': 'XHR', - 'api_payload_format': PAYLOAD_TYPE_BASE64, - 'app_host': 'https://mixpanel.com', - 'cdn': 'https://cdn.mxpnl.com', - 'cross_site_cookie': false, - 'cross_subdomain_cookie': true, - 'error_reporter': NOOP_FUNC, - 'persistence': 'cookie', - 'persistence_name': '', - 'cookie_domain': '', - 'cookie_name': '', - 'loaded': NOOP_FUNC, - 'mp_loader': null, - 'track_marketing': true, - 'track_pageview': false, - 'skip_first_touch_marketing': false, - 'store_google': true, - 'stop_utm_persistence': false, - 'save_referrer': true, - 'test': false, - 'verbose': false, - 'img': false, - 'debug': false, - 'track_links_timeout': 300, - 'cookie_expiration': 365, - 'upgrade': false, - 'disable_persistence': false, - 'disable_cookie': false, - 'secure_cookie': false, - 'ip': true, - 'opt_out_tracking_by_default': false, - 'opt_out_persistence_by_default': false, - 'opt_out_tracking_persistence_type': 'localStorage', - 'opt_out_tracking_cookie_prefix': null, - 'property_blacklist': [], - 'xhr_headers': {}, // { header: value, header2: value } - 'ignore_dnt': false, - 'batch_requests': true, - 'batch_size': 50, - 'batch_flush_interval_ms': 5000, - 'batch_request_timeout_ms': 90000, - 'batch_autostart': true, - 'hooks': {}, - 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), - 'record_block_selector': 'img, video', - 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes - 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), - 'record_mask_text_selector': '*', - 'record_max_ms': MAX_RECORDING_MS, - 'record_sessions_percent': 0, - 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' -}; - -var DOM_LOADED = false; - -/** - * Mixpanel Library Object - * @constructor - */ -var MixpanelLib = function() {}; - - -/** - * create_mplib(token:string, config:object, name:string) - * - * This function is used by the init method of MixpanelLib objects - * as well as the main initializer at the end of the JSLib (that - * initializes document.mixpanel as well as any additional instances - * declared before this file has loaded). - */ -var create_mplib = function(token, config, name) { - var instance, - target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; - - if (target && init_type === INIT_MODULE) { - instance = target; - } else { - if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); - return; - } - instance = new MixpanelLib(); - } - - instance._cached_groups = {}; // cache groups in a pool - - instance._init(token, config, name); - - instance['people'] = new MixpanelPeople(); - instance['people']._init(instance); - - if (!instance.get_config('skip_first_touch_marketing')) { - // We need null UTM params in the object because - // UTM parameters act as a tuple. If any UTM param - // is present, then we set all UTM params including - // empty ones together - var utm_params = _.info.campaignParams(null); - var initial_utm_params = {}; - var has_utm = false; - _.each(utm_params, function(utm_value, utm_key) { - initial_utm_params['initial_' + utm_key] = utm_value; - if (utm_value) { - has_utm = true; - } - }); - if (has_utm) { - instance['people'].set_once(initial_utm_params); - } - } - - // if any instance on the page has debug = true, we set the - // global debug to be true - Config.DEBUG = Config.DEBUG || instance.get_config('debug'); - - // if target is not defined, we called init after the lib already - // loaded, so there won't be an array of things to execute - if (!_.isUndefined(target) && _.isArray(target)) { - // Crunch through the people queue first - we queue this data up & - // flush on identify, so it's better to do all these operations first - instance._execute_array.call(instance['people'], target['people']); - instance._execute_array(target); - } - - return instance; -}; - -// Initialization methods - -/** - * This function initializes a new instance of the Mixpanel tracking object. - * All new instances are added to the main mixpanel object as sub properties (such as - * mixpanel.library_name) and also returned by this function. To define a - * second instance on the page, you would call: - * - * mixpanel.init('new token', { your: 'config' }, 'library_name'); - * - * and use it like so: - * - * mixpanel.library_name.track(...); - * - * @param {String} token Your Mixpanel API token - * @param {Object} [config] A dictionary of config options to override. See a list of default config options. - * @param {String} [name] The name for the new mixpanel instance that you want created - */ -MixpanelLib.prototype.init = function (token, config, name) { - if (_.isUndefined(name)) { - this.report_error('You must name your new library: init(token, config, name)'); - return; - } - if (name === PRIMARY_INSTANCE_NAME) { - this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); - return; - } - - var instance = create_mplib(token, config, name); - mixpanel_master[name] = instance; - instance._loaded(); - - return instance; -}; - -// mixpanel._init(token:string, config:object, name:string) -// -// This function sets up the current instance of the mixpanel -// library. The difference between this method and the init(...) -// method is this one initializes the actual instance, whereas the -// init(...) method sets up a new library and calls _init on it. -// -MixpanelLib.prototype._init = function(token, config, name) { - config = config || {}; - - this['__loaded'] = true; - this['config'] = {}; - - var variable_features = {}; - - // default to JSON payload for standard mixpanel.com API hosts - if (!('api_payload_format' in config)) { - var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; - if (api_host.match(/\.mixpanel\.com/)) { - variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; - } - } - - this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { - 'name': name, - 'token': token, - 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' - })); - - this['_jsc'] = NOOP_FUNC; - - this.__dom_loaded_queue = []; - this.__request_queue = []; - this.__disabled_events = []; - this._flags = { - 'disable_all_events': false, - 'identify_called': false - }; - - // set up request queueing/batching - this.request_batchers = {}; - this._batch_requests = this.get_config('batch_requests'); - if (this._batch_requests) { - if (!_.localStorage.is_supported(true) || !USE_XHR) { - this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); - _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); - _.localStorage.remove(batcher_config.queue_key); - }); - } else { - this.init_batchers(); - if (sendBeacon && win.addEventListener) { - // Before page closes or hides (user tabs away etc), attempt to flush any events - // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, - // events will not be removed from the persistent store; if the site is loaded again, - // the events will be flushed again on startup and deduplicated on the Mixpanel server - // side. - // There is no reliable way to capture only page close events, so we lean on the - // visibilitychange and pagehide events as recommended at - // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. - // These events fire when the user clicks away from the current page/tab, so will occur - // more frequently than page unload, but are the only mechanism currently for capturing - // this scenario somewhat reliably. - var flush_on_unload = _.bind(function() { - if (!this.request_batchers.events.stopped) { - this.request_batchers.events.flush({unloading: true}); - } - }, this); - win.addEventListener('pagehide', function(ev) { - if (ev['persisted']) { - flush_on_unload(); - } - }); - win.addEventListener('visibilitychange', function() { - if (document$1['visibilityState'] === 'hidden') { - flush_on_unload(); - } - }); - } - } - } - - this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); - this.unpersisted_superprops = {}; - this._gdpr_init(); - - var uuid = _.UUID(); - if (!this.get_distinct_id()) { - // There is no need to set the distinct id - // or the device id if something was already stored - // in the persitence - this.register_once({ - 'distinct_id': DEVICE_ID_PREFIX + uuid, - '$device_id': uuid - }, ''); - } - - var track_pageview_option = this.get_config('track_pageview'); - if (track_pageview_option) { - this._init_url_change_tracking(track_pageview_option); - } - - if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { - this.start_session_recording(); - } -}; - -MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { - if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); - return; - } - - var handleLoadedRecorder = _.bind(function() { - this._recorder = this._recorder || new win['__mp_recorder'](this); - this._recorder['startRecording'](); - }, this); - - if (_.isUndefined(win['__mp_recorder'])) { - load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); - } else { - handleLoadedRecorder(); - } -}); - -MixpanelLib.prototype.stop_session_recording = function () { - if (this._recorder) { - this._recorder['stopRecording'](); - } else { - console.critical('Session recorder module not loaded'); - } -}; - -MixpanelLib.prototype.get_session_recording_properties = function () { - var props = {}; - if (this._recorder) { - var replay_id = this._recorder['replayId']; - if (replay_id) { - props['$mp_replay_id'] = replay_id; - } - } - return props; -}; - -// Private methods - -MixpanelLib.prototype._loaded = function() { - this.get_config('loaded')(this); - this._set_default_superprops(); - this['people'].set_once(this['persistence'].get_referrer_info()); - - // `store_google` is now deprecated and previously stored UTM parameters are cleared - // from persistence by default. - if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { - var utm_params = _.info.campaignParams(null); - _.each(utm_params, function(_utm_value, utm_key) { - // We need to unregister persisted UTM parameters so old values - // are not mixed with the new UTM parameters - this.unregister(utm_key); - }.bind(this)); - } -}; - -// update persistence with info on referrer, UTM params, etc -MixpanelLib.prototype._set_default_superprops = function() { - this['persistence'].update_search_keyword(document$1.referrer); - // Registering super properties for UTM persistence by 'store_google' is deprecated. - if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { - this.register(_.info.campaignParams()); - } - if (this.get_config('save_referrer')) { - this['persistence'].update_referrer_info(document$1.referrer); - } -}; - -MixpanelLib.prototype._dom_loaded = function() { - _.each(this.__dom_loaded_queue, function(item) { - this._track_dom.apply(this, item); - }, this); - - if (!this.has_opted_out_tracking()) { - _.each(this.__request_queue, function(item) { - this._send_request.apply(this, item); - }, this); - } - - delete this.__dom_loaded_queue; - delete this.__request_queue; -}; - -MixpanelLib.prototype._track_dom = function(DomClass, args) { - if (this.get_config('img')) { - this.report_error('You can\'t use DOM tracking functions with img = true.'); - return false; - } - - if (!DOM_LOADED) { - this.__dom_loaded_queue.push([DomClass, args]); - return false; - } - - var dt = new DomClass().init(this); - return dt.track.apply(dt, args); -}; - -MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { - var previous_tracked_url = ''; - var tracked = this.track_pageview(); - if (tracked) { - previous_tracked_url = _.info.currentUrl(); - } - - if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { - win.addEventListener('popstate', function() { - win.dispatchEvent(new Event('mp_locationchange')); - }); - win.addEventListener('hashchange', function() { - win.dispatchEvent(new Event('mp_locationchange')); - }); - var nativePushState = win.history.pushState; - if (typeof nativePushState === 'function') { - win.history.pushState = function(state, unused, url) { - nativePushState.call(win.history, state, unused, url); - win.dispatchEvent(new Event('mp_locationchange')); - }; - } - var nativeReplaceState = win.history.replaceState; - if (typeof nativeReplaceState === 'function') { - win.history.replaceState = function(state, unused, url) { - nativeReplaceState.call(win.history, state, unused, url); - win.dispatchEvent(new Event('mp_locationchange')); - }; - } - win.addEventListener('mp_locationchange', function() { - var current_url = _.info.currentUrl(); - var should_track = false; - if (track_pageview_option === 'full-url') { - should_track = current_url !== previous_tracked_url; - } else if (track_pageview_option === 'url-with-path-and-query-string') { - should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; - } else if (track_pageview_option === 'url-with-path') { - should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; - } - - if (should_track) { - var tracked = this.track_pageview(); - if (tracked) { - previous_tracked_url = current_url; - } - } - }.bind(this)); - } -}; - -/** - * _prepare_callback() should be called by callers of _send_request for use - * as the callback argument. - * - * If there is no callback, this returns null. - * If we are going to make XHR/XDR requests, this returns a function. - * If we are going to use script tags, this returns a string to use as the - * callback GET param. - */ -MixpanelLib.prototype._prepare_callback = function(callback, data) { - if (_.isUndefined(callback)) { - return null; - } - - if (USE_XHR) { - var callback_function = function(response) { - callback(response, data); - }; - return callback_function; - } else { - // if the user gives us a callback, we store as a random - // property on this instances jsc function and update our - // callback string to reflect that. - var jsc = this['_jsc']; - var randomized_cb = '' + Math.floor(Math.random() * 100000000); - var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; - jsc[randomized_cb] = function(response) { - delete jsc[randomized_cb]; - callback(response, data); - }; - return callback_string; - } -}; - -MixpanelLib.prototype._send_request = function(url, data, options, callback) { - var succeeded = true; - - if (ENQUEUE_REQUESTS) { - this.__request_queue.push(arguments); - return succeeded; - } - - var DEFAULT_OPTIONS = { - method: this.get_config('api_method'), - transport: this.get_config('api_transport'), - verbose: this.get_config('verbose') - }; - var body_data = null; - - if (!callback && (_.isFunction(options) || typeof options === 'string')) { - callback = options; - options = null; - } - options = _.extend(DEFAULT_OPTIONS, options || {}); - if (!USE_XHR) { - options.method = 'GET'; - } - var use_post = options.method === 'POST'; - var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; - - // needed to correctly format responses - var verbose_mode = options.verbose; - if (data['verbose']) { verbose_mode = true; } - - if (this.get_config('test')) { data['test'] = 1; } - if (verbose_mode) { data['verbose'] = 1; } - if (this.get_config('img')) { data['img'] = 1; } - if (!USE_XHR) { - if (callback) { - data['callback'] = callback; - } else if (verbose_mode || this.get_config('test')) { - // Verbose output (from verbose mode, or an error in test mode) is a json blob, - // which by itself is not valid javascript. Without a callback, this verbose output will - // cause an error when returned via jsonp, so we force a no-op callback param. - // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 - data['callback'] = '(function(){})'; - } - } - - data['ip'] = this.get_config('ip')?1:0; - data['_'] = new Date().getTime().toString(); - - if (use_post) { - body_data = 'data=' + encodeURIComponent(data['data']); - delete data['data']; - } - - url += '?' + _.HTTPBuildQuery(data); - - var lib = this; - if ('img' in data) { - var img = document$1.createElement('img'); - img.src = url; - document$1.body.appendChild(img); - } else if (use_sendBeacon) { - try { - succeeded = sendBeacon(url, body_data); - } catch (e) { - lib.report_error(e); - succeeded = false; - } - try { - if (callback) { - callback(succeeded ? 1 : 0); - } - } catch (e) { - lib.report_error(e); - } - } else if (USE_XHR) { - try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - - var headers = this.get_config('xhr_headers'); - if (use_post) { - headers['Content-Type'] = 'application/x-www-form-urlencoded'; - } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); - } catch (e) { - lib.report_error(e); - succeeded = false; - } - } else { - var script = document$1.createElement('script'); - script.type = 'text/javascript'; - script.async = true; - script.defer = true; - script.src = url; - var s = document$1.getElementsByTagName('script')[0]; - s.parentNode.insertBefore(script, s); - } - - return succeeded; -}; - -/** - * _execute_array() deals with processing any mixpanel function - * calls that were called before the Mixpanel library were loaded - * (and are thus stored in an array so they can be called later) - * - * Note: we fire off all the mixpanel function calls && user defined - * functions BEFORE we fire off mixpanel tracking calls. This is so - * identify/register/set_config calls can properly modify early - * tracking calls. - * - * @param {Array} array - */ -MixpanelLib.prototype._execute_array = function(array) { - var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; - _.each(array, function(item) { - if (item) { - fn_name = item[0]; - if (_.isArray(fn_name)) { - tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() - } else if (typeof(item) === 'function') { - item.call(this); - } else if (_.isArray(item) && fn_name === 'alias') { - alias_calls.push(item); - } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { - tracking_calls.push(item); - } else { - other_calls.push(item); - } - } - }, this); - - var execute = function(calls, context) { - _.each(calls, function(item) { - if (_.isArray(item[0])) { - // chained call - var caller = context; - _.each(item, function(call) { - caller = caller[call[0]].apply(caller, call.slice(1)); - }); - } else { - this[item[0]].apply(this, item.slice(1)); - } - }, context); - }; - - execute(alias_calls, this); - execute(other_calls, this); - execute(tracking_calls, this); -}; - -// request queueing utils - -MixpanelLib.prototype.are_batchers_initialized = function() { - return !!this.request_batchers.events; -}; - -MixpanelLib.prototype.get_batcher_configs = function() { - var queue_prefix = '__mpq_' + this.get_config('token'); - var api_routes = this.get_config('api_routes'); - this._batcher_configs = this._batcher_configs || { - events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, - people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, - groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} - }; - return this._batcher_configs; -}; - -MixpanelLib.prototype.init_batchers = function() { - if (!this.are_batchers_initialized()) { - var batcher_for = _.bind(function(attrs) { - return new RequestBatcher( - attrs.queue_key, - { - libConfig: this['config'], - sendRequestFunc: _.bind(function(data, options, cb) { - this._send_request( - this.get_config('api_host') + attrs.endpoint, - this._encode_data_for_request(data), - options, - this._prepare_callback(cb, data) - ); - }, this), - beforeSendHook: _.bind(function(item) { - return this._run_hook('before_send_' + attrs.type, item); - }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) - } - ); - }, this); - var batcher_configs = this.get_batcher_configs(); - this.request_batchers = { - events: batcher_for(batcher_configs.events), - people: batcher_for(batcher_configs.people), - groups: batcher_for(batcher_configs.groups) - }; - } - if (this.get_config('batch_autostart')) { - this.start_batch_senders(); - } -}; - -MixpanelLib.prototype.start_batch_senders = function() { - this._batchers_were_started = true; - if (this.are_batchers_initialized()) { - this._batch_requests = true; - _.each(this.request_batchers, function(batcher) { - batcher.start(); - }); - } -}; - -MixpanelLib.prototype.stop_batch_senders = function() { - this._batch_requests = false; - _.each(this.request_batchers, function(batcher) { - batcher.stop(); - batcher.clear(); - }); -}; - -/** - * push() keeps the standard async-array-push - * behavior around after the lib is loaded. - * This is only useful for external integrations that - * do not wish to rely on our convenience methods - * (created in the snippet). - * - * ### Usage: - * mixpanel.push(['register', { a: 'b' }]); - * - * @param {Array} item A [function_name, args...] array to be executed - */ -MixpanelLib.prototype.push = function(item) { - this._execute_array([item]); -}; - -/** - * Disable events on the Mixpanel object. If passed no arguments, - * this function disables tracking of any event. If passed an - * array of event names, those events will be disabled, but other - * events will continue to be tracked. - * - * Note: this function does not stop other mixpanel functions from - * firing, such as register() or people.set(). - * - * @param {Array} [events] An array of event names to disable - */ -MixpanelLib.prototype.disable = function(events) { - if (typeof(events) === 'undefined') { - this._flags.disable_all_events = true; - } else { - this.__disabled_events = this.__disabled_events.concat(events); - } -}; - -MixpanelLib.prototype._encode_data_for_request = function(data) { - var encoded_data = _.JSONEncode(data); - if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { - encoded_data = _.base64Encode(encoded_data); - } - return {'data': encoded_data}; -}; - -// internal method for handling track vs batch-enqueue logic -MixpanelLib.prototype._track_or_batch = function(options, callback) { - var truncated_data = _.truncate(options.data, 255); - var endpoint = options.endpoint; - var batcher = options.batcher; - var should_send_immediately = options.should_send_immediately; - var send_request_options = options.send_request_options || {}; - callback = callback || NOOP_FUNC; - - var request_enqueued_or_initiated = true; - var send_request_immediately = _.bind(function() { - if (!send_request_options.skip_hooks) { - truncated_data = this._run_hook('before_send_' + options.type, truncated_data); - } - if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); - return this._send_request( - endpoint, - this._encode_data_for_request(truncated_data), - send_request_options, - this._prepare_callback(callback, truncated_data) - ); - } else { - return null; - } - }, this); - - if (this._batch_requests && !should_send_immediately) { - batcher.enqueue(truncated_data, function(succeeded) { - if (succeeded) { - callback(1, truncated_data); - } else { - send_request_immediately(); - } - }); - } else { - request_enqueued_or_initiated = send_request_immediately(); - } - - return request_enqueued_or_initiated && truncated_data; -}; - -/** - * Track an event. This is the most important and - * frequently used Mixpanel function. - * - * ### Usage: - * - * // track an event named 'Registered' - * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); - * - * // track an event using navigator.sendBeacon - * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); - * - * To track link clicks or form submissions, see track_links() or track_forms(). - * - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Object} [options] Optional configuration for this track request. - * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). - * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object - * with the tracking payload sent to the API server is returned; otherwise false. - */ -MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { - if (!callback && typeof options === 'function') { - callback = options; - options = null; - } - options = options || {}; - var transport = options['transport']; // external API, don't minify 'transport' prop - if (transport) { - options.transport = transport; // 'transport' prop name can be minified internally - } - var should_send_immediately = options['send_immediately']; - if (typeof callback !== 'function') { - callback = NOOP_FUNC; - } - - if (_.isUndefined(event_name)) { - this.report_error('No event name provided to mixpanel.track'); - return; - } - - if (this._event_is_disabled(event_name)) { - callback(0); - return; - } - - // set defaults - properties = _.extend({}, properties); - properties['token'] = this.get_config('token'); - - // set $duration if time_event was previously called for this event - var start_timestamp = this['persistence'].remove_event_timer(event_name); - if (!_.isUndefined(start_timestamp)) { - var duration_in_ms = new Date().getTime() - start_timestamp; - properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); - } - - this._set_default_superprops(); - - var marketing_properties = this.get_config('track_marketing') - ? _.info.marketingParams() - : {}; - - // note: extend writes to the first object, so lets make sure we - // don't write to the persistence properties object and info - // properties object by passing in a new object - - // update properties with pageview info and super-properties - properties = _.extend( - {}, - _.info.properties({'mp_loader': this.get_config('mp_loader')}), - marketing_properties, - this['persistence'].properties(), - this.unpersisted_superprops, - this.get_session_recording_properties(), - properties - ); - - var property_blacklist = this.get_config('property_blacklist'); - if (_.isArray(property_blacklist)) { - _.each(property_blacklist, function(blacklisted_prop) { - delete properties[blacklisted_prop]; - }); - } else { - this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); - } - - var data = { - 'event': event_name, - 'properties': properties - }; - var ret = this._track_or_batch({ - type: 'events', - data: data, - endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], - batcher: this.request_batchers.events, - should_send_immediately: should_send_immediately, - send_request_options: options - }, callback); - - return ret; -}); - -/** - * Register the current user into one/many groups. - * - * ### Usage: - * - * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs - * mixpanel.set_group('company', 'mixpanel') - * mixpanel.set_group('company', 128746312) - * - * @param {String} group_key Group key - * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - * - */ -MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { - if (!_.isArray(group_ids)) { - group_ids = [group_ids]; - } - var prop = {}; - prop[group_key] = group_ids; - this.register(prop); - return this['people'].set(group_key, group_ids, callback); -}); - -/** - * Add a new group for this user. - * - * ### Usage: - * - * mixpanel.add_group('company', 'mixpanel') - * - * @param {String} group_key Group key - * @param {*} group_id A valid Mixpanel property type - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { - var old_values = this.get_property(group_key); - var prop = {}; - if (old_values === undefined) { - prop[group_key] = [group_id]; - this.register(prop); - } else { - if (old_values.indexOf(group_id) === -1) { - old_values.push(group_id); - prop[group_key] = old_values; - this.register(prop); - } - } - return this['people'].union(group_key, group_id, callback); -}); - -/** - * Remove a group from this user. - * - * ### Usage: - * - * mixpanel.remove_group('company', 'mixpanel') - * - * @param {String} group_key Group key - * @param {*} group_id A valid Mixpanel property type - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { - var old_value = this.get_property(group_key); - // if the value doesn't exist, the persistent store is unchanged - if (old_value !== undefined) { - var idx = old_value.indexOf(group_id); - if (idx > -1) { - old_value.splice(idx, 1); - this.register({group_key: old_value}); - } - if (old_value.length === 0) { - this.unregister(group_key); - } - } - return this['people'].remove(group_key, group_id, callback); -}); - -/** - * Track an event with specific groups. - * - * ### Usage: - * - * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) - * - * @param {String} event_name The name of the event (see `mixpanel.track()`) - * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) - * @param {Object=} groups An object mapping group name keys to one or more values - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { - var tracking_props = _.extend({}, properties || {}); - _.each(groups, function(v, k) { - if (v !== null && v !== undefined) { - tracking_props[k] = v; - } - }); - return this.track(event_name, tracking_props, callback); -}); - -MixpanelLib.prototype._create_map_key = function (group_key, group_id) { - return group_key + '_' + JSON.stringify(group_id); -}; - -MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { - delete this._cached_groups[this._create_map_key(group_key, group_id)]; -}; - -/** - * Look up reference to a Mixpanel group - * - * ### Usage: - * - * mixpanel.get_group(group_key, group_id) - * - * @param {String} group_key Group key - * @param {Object} group_id A valid Mixpanel property type - * @returns {Object} A MixpanelGroup identifier - */ -MixpanelLib.prototype.get_group = function (group_key, group_id) { - var map_key = this._create_map_key(group_key, group_id); - var group = this._cached_groups[map_key]; - if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { - group = new MixpanelGroup(); - group._init(this, group_key, group_id); - this._cached_groups[map_key] = group; - } - return group; -}; - -/** - * Track a default Mixpanel page view event, which includes extra default event properties to - * improve page view data. - * - * ### Usage: - * - * // track a default $mp_web_page_view event - * mixpanel.track_pageview(); - * - * // track a page view event with additional event properties - * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); - * - * // example approach to track page views on different page types as event properties - * mixpanel.track_pageview({'page': 'pricing'}); - * mixpanel.track_pageview({'page': 'homepage'}); - * - * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for - * // individual pages on the same site or product. Use cases for custom event_name may be page - * // views on different products or internal applications that are considered completely separate - * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); - * - * ### Notes: - * - * The `config.track_pageview` option for mixpanel.init() - * may be turned on for tracking page loads automatically. - * - * // track only page loads - * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); - * - * // track when the URL changes in any manner - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); - * - * // track when the URL changes, ignoring any changes in the hash part - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); - * - * // track when the path changes, ignoring any query parameter or hash changes - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); - * - * @param {Object} [properties] An optional set of additional properties to send with the page view event - * @param {Object} [options] Page view tracking options - * @param {String} [options.event_name] - Alternate name for the tracking event - * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object - * with the tracking payload sent to the API server is returned; otherwise false. - */ -MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { - if (typeof properties !== 'object') { - properties = {}; - } - options = options || {}; - var event_name = options['event_name'] || '$mp_web_page_view'; - - var default_page_properties = _.extend( - _.info.mpPageViewProperties(), - _.info.campaignParams(), - _.info.clickParams() - ); - - var event_properties = _.extend( - {}, - default_page_properties, - properties - ); - - return this.track(event_name, event_properties); -}); - -/** - * Track clicks on a set of document elements. Selector must be a - * valid query. Elements must exist on the page at the time track_links is called. - * - * ### Usage: - * - * // track click for link id #nav - * mixpanel.track_links('#nav', 'Clicked Nav Link'); - * - * ### Notes: - * - * This function will wait up to 300 ms for the Mixpanel - * servers to respond. If they have not responded by that time - * it will head to the link without ensuring that your event - * has been tracked. To configure this timeout please see the - * set_config() documentation below. - * - * If you pass a function in as the properties argument, the - * function will receive the DOMElement that triggered the - * event as an argument. You are expected to return an object - * from the function; any properties defined on this object - * will be sent to mixpanel as event properties. - * - * @type {Function} - * @param {Object|String} query A valid DOM query, element or jQuery-esque list - * @param {String} event_name The name of the event to track - * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement - */ -MixpanelLib.prototype.track_links = function() { - return this._track_dom.call(this, LinkTracker, arguments); -}; - -/** - * Track form submissions. Selector must be a valid query. - * - * ### Usage: - * - * // track submission for form id 'register' - * mixpanel.track_forms('#register', 'Created Account'); - * - * ### Notes: - * - * This function will wait up to 300 ms for the mixpanel - * servers to respond, if they have not responded by that time - * it will head to the link without ensuring that your event - * has been tracked. To configure this timeout please see the - * set_config() documentation below. - * - * If you pass a function in as the properties argument, the - * function will receive the DOMElement that triggered the - * event as an argument. You are expected to return an object - * from the function; any properties defined on this object - * will be sent to mixpanel as event properties. - * - * @type {Function} - * @param {Object|String} query A valid DOM query, element or jQuery-esque list - * @param {String} event_name The name of the event to track - * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement - */ -MixpanelLib.prototype.track_forms = function() { - return this._track_dom.call(this, FormTracker, arguments); -}; - -/** - * Time an event by including the time between this call and a - * later 'track' call for the same event in the properties sent - * with the event. - * - * ### Usage: - * - * // time an event named 'Registered' - * mixpanel.time_event('Registered'); - * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); - * - * When called for a particular event name, the next track call for that event - * name will include the elapsed time between the 'time_event' and 'track' - * calls. This value is stored as seconds in the '$duration' property. - * - * @param {String} event_name The name of the event. - */ -MixpanelLib.prototype.time_event = function(event_name) { - if (_.isUndefined(event_name)) { - this.report_error('No event name provided to mixpanel.time_event'); - return; - } - - if (this._event_is_disabled(event_name)) { - return; - } - - this['persistence'].set_event_timer(event_name, new Date().getTime()); -}; - -var REGISTER_DEFAULTS = { - 'persistent': true -}; -/** - * Helper to parse options param for register methods, maintaining - * legacy support for plain "days" param instead of options object - * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods - * @returns {Object} options object - */ -var options_for_register = function(days_or_options) { - var options; - if (_.isObject(days_or_options)) { - options = days_or_options; - } else if (!_.isUndefined(days_or_options)) { - options = {'days': days_or_options}; - } else { - options = {}; - } - return _.extend({}, REGISTER_DEFAULTS, options); -}; - -/** - * Register a set of super properties, which are included with all - * events. This will overwrite previous super property values. - * - * ### Usage: - * - * // register 'Gender' as a super property - * mixpanel.register({'Gender': 'Female'}); - * - * // register several super properties when a user signs up - * mixpanel.register({ - * 'Email': 'jdoe@example.com', - * 'Account Type': 'Free' - * }); - * - * // register only for the current pageload - * mixpanel.register({'Name': 'Pat'}, {persistent: false}); - * - * @param {Object} properties An associative array of properties to store about the user - * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.register = function(props, days_or_options) { - var options = options_for_register(days_or_options); - if (options['persistent']) { - this['persistence'].register(props, options['days']); - } else { - _.extend(this.unpersisted_superprops, props); - } -}; - -/** - * Register a set of super properties only once. This will not - * overwrite previous super property values, unlike register(). - * - * ### Usage: - * - * // register a super property for the first time only - * mixpanel.register_once({ - * 'First Login Date': new Date().toISOString() - * }); - * - * // register once, only for the current pageload - * mixpanel.register_once({ - * 'First interaction time': new Date().toISOString() - * }, 'None', {persistent: false}); - * - * ### Notes: - * - * If default_value is specified, current super properties - * with that value will be overwritten. - * - * @param {Object} properties An associative array of properties to store about the user - * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' - * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { - var options = options_for_register(days_or_options); - if (options['persistent']) { - this['persistence'].register_once(props, default_value, options['days']); - } else { - if (typeof(default_value) === 'undefined') { - default_value = 'None'; - } - _.each(props, function(val, prop) { - if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { - this.unpersisted_superprops[prop] = val; - } - }, this); - } -}; - -/** - * Delete a super property stored with the current user. - * - * @param {String} property The name of the super property to remove - * @param {Object} [options] - * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.unregister = function(property, options) { - options = options_for_register(options); - if (options['persistent']) { - this['persistence'].unregister(property); - } else { - delete this.unpersisted_superprops[property]; - } -}; - -MixpanelLib.prototype._register_single = function(prop, value) { - var props = {}; - props[prop] = value; - this.register(props); -}; - -/** - * Identify a user with a unique ID to track user activity across - * devices, tie a user to their events, and create a user profile. - * If you never call this method, unique visitors are tracked using - * a UUID generated the first time they visit the site. - * - * Call identify when you know the identity of the current user, - * typically after login or signup. We recommend against using - * identify for anonymous visitors to your site. - * - * ### Notes: - * If your project has - * ID Merge - * enabled, the identify method will connect pre- and - * post-authentication events when appropriate. - * - * If your project does not have ID Merge enabled, identify will - * change the user's local distinct_id to the unique ID you pass. - * Events tracked prior to authentication will not be connected - * to the same user identity. If ID Merge is disabled, alias can - * be used to connect pre- and post-registration events. - * - * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. - */ -MixpanelLib.prototype.identify = function( - new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback -) { - // Optional Parameters - // _set_callback:function A callback to be run if and when the People set queue is flushed - // _add_callback:function A callback to be run if and when the People add queue is flushed - // _append_callback:function A callback to be run if and when the People append queue is flushed - // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed - // _union_callback:function A callback to be run if and when the People union queue is flushed - // _unset_callback:function A callback to be run if and when the People unset queue is flushed - - var previous_distinct_id = this.get_distinct_id(); - if (new_distinct_id && previous_distinct_id !== new_distinct_id) { - // we allow the following condition if previous distinct_id is same as new_distinct_id - // so that you can force flush people updates for anonymous profiles. - if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { - this.report_error('distinct_id cannot have $device: prefix'); - return -1; - } - this.register({'$user_id': new_distinct_id}); - } - - if (!this.get_property('$device_id')) { - // The persisted distinct id might not actually be a device id at all - // it might be a distinct id of the user from before - var device_id = previous_distinct_id; - this.register_once({ - '$had_persisted_distinct_id': true, - '$device_id': device_id - }, ''); - } - - // identify only changes the distinct id if it doesn't match either the existing or the alias; - // if it's new, blow away the alias as well. - if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { - this.unregister(ALIAS_ID_KEY); - this.register({'distinct_id': new_distinct_id}); - } - this._flags.identify_called = true; - // Flush any queued up people requests - this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); - - // send an $identify event any time the distinct_id is changing - logic on the server - // will determine whether or not to do anything with it. - if (new_distinct_id !== previous_distinct_id) { - this.track('$identify', { - 'distinct_id': new_distinct_id, - '$anon_distinct_id': previous_distinct_id - }, {skip_hooks: true}); - } -}; - -/** - * Clears super properties and generates a new random distinct_id for this instance. - * Useful for clearing data when a user logs out. - */ -MixpanelLib.prototype.reset = function() { - this['persistence'].clear(); - this._flags.identify_called = false; - var uuid = _.UUID(); - this.register_once({ - 'distinct_id': DEVICE_ID_PREFIX + uuid, - '$device_id': uuid - }, ''); -}; - -/** - * Returns the current distinct id of the user. This is either the id automatically - * generated by the library or the id that has been passed by a call to identify(). - * - * ### Notes: - * - * get_distinct_id() can only be called after the Mixpanel library has finished loading. - * init() has a loaded function available to handle this automatically. For example: - * - * // set distinct_id after the mixpanel library has loaded - * mixpanel.init('YOUR PROJECT TOKEN', { - * loaded: function(mixpanel) { - * distinct_id = mixpanel.get_distinct_id(); - * } - * }); - */ -MixpanelLib.prototype.get_distinct_id = function() { - return this.get_property('distinct_id'); -}; - -/** - * The alias method creates an alias which Mixpanel will use to - * remap one id to another. Multiple aliases can point to the - * same identifier. - * - * The following is a valid use of alias: - * - * mixpanel.alias('new_id', 'existing_id'); - * // You can add multiple id aliases to the existing ID - * mixpanel.alias('newer_id', 'existing_id'); - * - * Aliases can also be chained - the following is a valid example: - * - * mixpanel.alias('new_id', 'existing_id'); - * // chain newer_id - new_id - existing_id - * mixpanel.alias('newer_id', 'new_id'); - * - * Aliases cannot point to multiple identifiers - the following - * example will not work: - * - * mixpanel.alias('new_id', 'existing_id'); - * // this is invalid as 'new_id' already points to 'existing_id' - * mixpanel.alias('new_id', 'newer_id'); - * - * ### Notes: - * - * If your project does not have - * ID Merge - * enabled, the best practice is to call alias once when a unique - * ID is first created for a user (e.g., when a user first registers - * for an account). Do not use alias multiple times for a single - * user without ID Merge enabled. - * - * @param {String} alias A unique identifier that you want to use for this user in the future. - * @param {String} [original] The current identifier being used for this user. - */ -MixpanelLib.prototype.alias = function(alias, original) { - // If the $people_distinct_id key exists in persistence, there has been a previous - // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with - // this ID, as it will duplicate users. - if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { - this.report_error('Attempting to create alias for existing People user - aborting.'); - return -2; - } - - var _this = this; - if (_.isUndefined(original)) { - original = this.get_distinct_id(); - } - if (alias !== original) { - this._register_single(ALIAS_ID_KEY, alias); - return this.track('$create_alias', { - 'alias': alias, - 'distinct_id': original - }, { - skip_hooks: true - }, function() { - // Flush the people queue - _this.identify(alias); - }); - } else { - this.report_error('alias matches current distinct_id - skipping api call.'); - this.identify(alias); - return -1; - } -}; - -/** - * Provide a string to recognize the user by. The string passed to - * this method will appear in the Mixpanel Streams product rather - * than an automatically generated name. Name tags do not have to - * be unique. - * - * This value will only be included in Streams data. - * - * @param {String} name_tag A human readable name for the user - * @deprecated - */ -MixpanelLib.prototype.name_tag = function(name_tag) { - this._register_single('mp_name_tag', name_tag); -}; - -/** - * Update the configuration of a mixpanel library instance. - * - * The default config is: - * - * { - * // host for requests (customizable for e.g. a local proxy) - * api_host: 'https://api-js.mixpanel.com', - * - * // endpoints for different types of requests - * api_routes: { - * track: 'track/', - * engage: 'engage/', - * groups: 'groups/', - * } - * - * // HTTP method for tracking requests - * api_method: 'POST' - * - * // transport for sending requests ('XHR' or 'sendBeacon') - * // NB: sendBeacon should only be used for scenarios such as - * // page unload where a "best-effort" attempt to send is - * // acceptable; the sendBeacon API does not support callbacks - * // or any way to know the result of the request. Mixpanel - * // tracking via sendBeacon will not support any event- - * // batching or retry mechanisms. - * api_transport: 'XHR' - * - * // request-batching/queueing/retry - * batch_requests: true, - * - * // maximum number of events/updates to send in a single - * // network request - * batch_size: 50, - * - * // milliseconds to wait between sending batch requests - * batch_flush_interval_ms: 5000, - * - * // milliseconds to wait for network responses to batch requests - * // before they are considered timed-out and retried - * batch_request_timeout_ms: 90000, - * - * // override value for cookie domain, only useful for ensuring - * // correct cross-subdomain cookies on unusual domains like - * // subdomain.mainsite.avocat.fr; NB this cannot be used to - * // set cookies on a different domain than the current origin - * cookie_domain: '' - * - * // super properties cookie expiration (in days) - * cookie_expiration: 365 - * - * // if true, cookie will be set with SameSite=None; Secure - * // this is only useful in special situations, like embedded - * // 3rd-party iframes that set up a Mixpanel instance - * cross_site_cookie: false - * - * // super properties span subdomains - * cross_subdomain_cookie: true - * - * // debug mode - * debug: false - * - * // if this is true, the mixpanel cookie or localStorage entry - * // will be deleted, and no user persistence will take place - * disable_persistence: false - * - * // if this is true, Mixpanel will automatically determine - * // City, Region and Country data using the IP address of - * //the client - * ip: true - * - * // opt users out of tracking by this Mixpanel instance by default - * opt_out_tracking_by_default: false - * - * // opt users out of browser data storage by this Mixpanel instance by default - * opt_out_persistence_by_default: false - * - * // persistence mechanism used by opt-in/opt-out methods - cookie - * // or localStorage - falls back to cookie if localStorage is unavailable - * opt_out_tracking_persistence_type: 'localStorage' - * - * // customize the name of cookie/localStorage set by opt-in/opt-out methods - * opt_out_tracking_cookie_prefix: null - * - * // type of persistent store for super properties (cookie/ - * // localStorage) if set to 'localStorage', any existing - * // mixpanel cookie value with the same persistence_name - * // will be transferred to localStorage and deleted - * persistence: 'cookie' - * - * // name for super properties persistent store - * persistence_name: '' - * - * // names of properties/superproperties which should never - * // be sent with track() calls - * property_blacklist: [] - * - * // if this is true, mixpanel cookies will be marked as - * // secure, meaning they will only be transmitted over https - * secure_cookie: false - * - * // disables enriching user profiles with first touch marketing data - * skip_first_touch_marketing: false - * - * // the amount of time track_links will - * // wait for Mixpanel's servers to respond - * track_links_timeout: 300 - * - * // adds any UTM parameters and click IDs present on the page to any events fired - * track_marketing: true - * - * // enables automatic page view tracking using default page view events through - * // the track_pageview() method - * track_pageview: false - * - * // if you set upgrade to be true, the library will check for - * // a cookie from our old js library and import super - * // properties from it, then the old cookie is deleted - * // The upgrade config option only works in the initialization, - * // so make sure you set it when you create the library. - * upgrade: false - * - * // extra HTTP request headers to set for each API request, in - * // the format {'Header-Name': value} - * xhr_headers: {} - * - * // whether to ignore or respect the web browser's Do Not Track setting - * ignore_dnt: false - * } - * - * - * @param {Object} config A dictionary of new configuration values to update - */ -MixpanelLib.prototype.set_config = function(config) { - if (_.isObject(config)) { - _.extend(this['config'], config); - - var new_batch_size = config['batch_size']; - if (new_batch_size) { - _.each(this.request_batchers, function(batcher) { - batcher.resetBatchSize(); - }); - } - - if (!this.get_config('persistence_name')) { - this['config']['persistence_name'] = this['config']['cookie_name']; - } - if (!this.get_config('disable_persistence')) { - this['config']['disable_persistence'] = this['config']['disable_cookie']; - } - - if (this['persistence']) { - this['persistence'].update_config(this['config']); - } - Config.DEBUG = Config.DEBUG || this.get_config('debug'); - } -}; - -/** - * returns the current config object for the library. - */ -MixpanelLib.prototype.get_config = function(prop_name) { - return this['config'][prop_name]; -}; - -/** - * Fetch a hook function from config, with safe default, and run it - * against the given arguments - * @param {string} hook_name which hook to retrieve - * @returns {any|null} return value of user-provided hook, or null if nothing was returned - */ -MixpanelLib.prototype._run_hook = function(hook_name) { - var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); - if (typeof ret === 'undefined') { - this.report_error(hook_name + ' hook did not return a value'); - ret = null; - } - return ret; -}; - -/** - * Returns the value of the super property named property_name. If no such - * property is set, get_property() will return the undefined value. - * - * ### Notes: - * - * get_property() can only be called after the Mixpanel library has finished loading. - * init() has a loaded function available to handle this automatically. For example: - * - * // grab value for 'user_id' after the mixpanel library has loaded - * mixpanel.init('YOUR PROJECT TOKEN', { - * loaded: function(mixpanel) { - * user_id = mixpanel.get_property('user_id'); - * } - * }); - * - * @param {String} property_name The name of the super property you want to retrieve - */ -MixpanelLib.prototype.get_property = function(property_name) { - return this['persistence'].load_prop([property_name]); -}; - -MixpanelLib.prototype.toString = function() { - var name = this.get_config('name'); - if (name !== PRIMARY_INSTANCE_NAME) { - name = PRIMARY_INSTANCE_NAME + '.' + name; - } - return name; -}; - -MixpanelLib.prototype._event_is_disabled = function(event_name) { - return _.isBlockedUA(userAgent) || - this._flags.disable_all_events || - _.include(this.__disabled_events, event_name); -}; - -// perform some housekeeping around GDPR opt-in/out state -MixpanelLib.prototype._gdpr_init = function() { - var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; - - // try to convert opt-in/out cookies to localStorage if possible - if (is_localStorage_requested && _.localStorage.is_supported()) { - if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { - this.opt_in_tracking({'enable_persistence': false}); - } - if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { - this.opt_out_tracking({'clear_persistence': false}); - } - this.clear_opt_in_out_tracking({ - 'persistence_type': 'cookie', - 'enable_persistence': false - }); - } - - // check whether the user has already opted out - if so, clear & disable persistence - if (this.has_opted_out_tracking()) { - this._gdpr_update_persistence({'clear_persistence': true}); - - // check whether we should opt out by default - // note: we don't clear persistence here by default since opt-out default state is often - // used as an initial state while GDPR information is being collected - } else if (!this.has_opted_in_tracking() && ( - this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') - )) { - _.cookie.remove('mp_optout'); - this.opt_out_tracking({ - 'clear_persistence': this.get_config('opt_out_persistence_by_default') - }); - } -}; - -/** - * Enable or disable persistence based on options - * only enable/disable if persistence is not already in this state - * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it - * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence - */ -MixpanelLib.prototype._gdpr_update_persistence = function(options) { - var disabled; - if (options && options['clear_persistence']) { - disabled = true; - } else if (options && options['enable_persistence']) { - disabled = false; - } else { - return; - } - - if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { - this['persistence'].set_disabled(disabled); - } - - if (disabled) { - this.stop_batch_senders(); - } else { - // only start batchers after opt-in if they have previously been started - // in order to avoid unintentionally starting up batching for the first time - if (this._batchers_were_started) { - this.start_batch_senders(); - } - } -}; - -// call a base gdpr function after constructing the appropriate token and options args -MixpanelLib.prototype._gdpr_call_func = function(func, options) { - options = _.extend({ - 'track': _.bind(this.track, this), - 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), - 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), - 'cookie_expiration': this.get_config('cookie_expiration'), - 'cross_site_cookie': this.get_config('cross_site_cookie'), - 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), - 'cookie_domain': this.get_config('cookie_domain'), - 'secure_cookie': this.get_config('secure_cookie'), - 'ignore_dnt': this.get_config('ignore_dnt') - }, options); - - // check if localStorage can be used for recording opt out status, fall back to cookie if not - if (!_.localStorage.is_supported()) { - options['persistence_type'] = 'cookie'; - } - - return func(this.get_config('token'), { - track: options['track'], - trackEventName: options['track_event_name'], - trackProperties: options['track_properties'], - persistenceType: options['persistence_type'], - persistencePrefix: options['cookie_prefix'], - cookieDomain: options['cookie_domain'], - cookieExpiration: options['cookie_expiration'], - crossSiteCookie: options['cross_site_cookie'], - crossSubdomainCookie: options['cross_subdomain_cookie'], - secureCookie: options['secure_cookie'], - ignoreDnt: options['ignore_dnt'] - }); -}; - -/** - * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // opt user in - * mixpanel.opt_in_tracking(); - * - * // opt user in with specific event name, properties, cookie configuration - * mixpanel.opt_in_tracking({ - * track_event_name: 'User opted in', - * track_event_properties: { - * 'Email': 'jdoe@example.com' - * }, - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) - * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action - * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action - * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.opt_in_tracking = function(options) { - options = _.extend({ - 'enable_persistence': true - }, options); - - this._gdpr_call_func(optIn, options); - this._gdpr_update_persistence(options); -}; - -/** - * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // opt user out - * mixpanel.opt_out_tracking(); - * - * // opt user out with different cookie configuration from Mixpanel instance - * mixpanel.opt_out_tracking({ - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out - * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.opt_out_tracking = function(options) { - options = _.extend({ - 'clear_persistence': true, - 'delete_user': true - }, options); - - // delete user and clear charges since these methods may be disabled by opt-out - if (options['delete_user'] && this['people'] && this['people']._identify_called()) { - this['people'].delete_user(); - this['people'].clear_charges(); - } - - this._gdpr_call_func(optOut, options); - this._gdpr_update_persistence(options); -}; - -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * var has_opted_in = mixpanel.has_opted_in_tracking(); - * // use has_opted_in value - * - * @param {Object} [options] A dictionary of config options to override - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @returns {boolean} current opt-in status - */ -MixpanelLib.prototype.has_opted_in_tracking = function(options) { - return this._gdpr_call_func(hasOptedIn, options); -}; - -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * var has_opted_out = mixpanel.has_opted_out_tracking(); - * // use has_opted_out value - * - * @param {Object} [options] A dictionary of config options to override - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @returns {boolean} current opt-out status - */ -MixpanelLib.prototype.has_opted_out_tracking = function(options) { - return this._gdpr_call_func(hasOptedOut, options); -}; - -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // clear user's opt-in/out status - * mixpanel.clear_opt_in_out_tracking(); - * - * // clear user's opt-in/out status with specific cookie configuration - should match - * // configuration used when opt_in_tracking/opt_out_tracking methods were called. - * mixpanel.clear_opt_in_out_tracking({ - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { - options = _.extend({ - 'enable_persistence': true - }, options); - - this._gdpr_call_func(clearOptInOut, options); - this._gdpr_update_persistence(options); -}; - -MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); - try { - if (!err && !(msg instanceof Error)) { - msg = new Error(msg); - } - this.get_config('error_reporter')(msg, err); - } catch(err) { - console.error(err); - } -}; - -// EXPORTS (for closure compiler) - -// MixpanelLib Exports -MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; -MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; -MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; -MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; -MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; -MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; -MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; -MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; -MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; -MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; -MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; -MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; -MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; -MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; -MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; -MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; -MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; -MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; -MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; -MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; -MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; -MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; -MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; -MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; -MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; -MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; -MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; -MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; -MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; -MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; -MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; -MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; -MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; -MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; -MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; - -// MixpanelPersistence Exports -MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; -MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; -MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; -MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; -MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; - - -var instances = {}; -var extend_mp = function() { - // add all the sub mixpanel instances - _.each(instances, function(instance, name) { - if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } - }); - - // add private functions as _ - mixpanel_master['_'] = _; -}; - -var override_mp_init_func = function() { - // we override the snippets init function to handle the case where a - // user initializes the mixpanel library after the script loads & runs - mixpanel_master['init'] = function(token, config, name) { - if (name) { - // initialize a sub library - if (!mixpanel_master[name]) { - mixpanel_master[name] = instances[name] = create_mplib(token, config, name); - mixpanel_master[name]._loaded(); - } - return mixpanel_master[name]; - } else { - var instance = mixpanel_master; - - if (instances[PRIMARY_INSTANCE_NAME]) { - // main mixpanel lib already initialized - instance = instances[PRIMARY_INSTANCE_NAME]; - } else if (token) { - // intialize the main mixpanel lib - instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); - instance._loaded(); - instances[PRIMARY_INSTANCE_NAME] = instance; - } - - mixpanel_master = instance; - if (init_type === INIT_SNIPPET) { - win[PRIMARY_INSTANCE_NAME] = mixpanel_master; - } - extend_mp(); - } - }; -}; - -var add_dom_loaded_handler = function() { - // Cross browser DOM Loaded support - function dom_loaded_handler() { - // function flag since we only want to execute this once - if (dom_loaded_handler.done) { return; } - dom_loaded_handler.done = true; - - DOM_LOADED = true; - ENQUEUE_REQUESTS = false; - - _.each(instances, function(inst) { - inst._dom_loaded(); - }); - } - - function do_scroll_check() { - try { - document$1.documentElement.doScroll('left'); - } catch(e) { - setTimeout(do_scroll_check, 1); - return; - } - - dom_loaded_handler(); - } - - if (document$1.addEventListener) { - if (document$1.readyState === 'complete') { - // safari 4 can fire the DOMContentLoaded event before loading all - // external JS (including this file). you will see some copypasta - // on the internet that checks for 'complete' and 'loaded', but - // 'loaded' is an IE thing - dom_loaded_handler(); - } else { - document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); - } - } else if (document$1.attachEvent) { - // IE - document$1.attachEvent('onreadystatechange', dom_loaded_handler); - - // check to make sure we arn't in a frame - var toplevel = false; - try { - toplevel = win.frameElement === null; - } catch(e) { - // noop - } - - if (document$1.documentElement.doScroll && toplevel) { - do_scroll_check(); - } - } - - // fallback handler, always will work - _.register_event(win, 'load', dom_loaded_handler, true); -}; - -function init_as_module(bundle_loader) { - load_extra_bundle = bundle_loader; - init_type = INIT_MODULE; - mixpanel_master = new MixpanelLib(); - - override_mp_init_func(); - mixpanel_master['init'](); - add_dom_loaded_handler(); - - return mixpanel_master; -} - -// For loading separate bundles asynchronously via script tag - -// For builds that do NOT want any extra bundles (e.g. session recorder) -// and just the main SDK, throw an error when trying to load a separate bundle. -// eslint-disable-next-line no-unused-vars -function loadThrowError (src, _onload) { - throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); -} - -/* eslint camelcase: "off" */ - -var mixpanel = init_as_module(loadThrowError); - -module.exports = mixpanel; From 6e1ecaa10a7a0d8e843ef96d612860b0fff6ed91 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Wed, 17 Jul 2024 21:46:45 +0000 Subject: [PATCH 44/48] comment for posterity --- src/recorder/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/recorder/index.js b/src/recorder/index.js index 5716897a..0da19332 100644 --- a/src/recorder/index.js +++ b/src/recorder/index.js @@ -130,6 +130,8 @@ MixpanelRecorder.prototype._onOptOut = function (code) { MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. if (response.status === 200) { this.seqNo++; } From f510bb304d3e7dc6e7bd0641ef40e21cda75c9b8 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Thu, 18 Jul 2024 21:41:39 +0000 Subject: [PATCH 45/48] fix assertion --- dist/mixpanel-core.cjs.js | 245 +- dist/mixpanel-main.cjs.js | 6330 ++++++++++++ dist/mixpanel-recorder.js | 1065 +- dist/mixpanel-recorder.min.js | 18 +- dist/mixpanel-with-async-recorder.cjs.js | 245 +- dist/mixpanel.amd.js | 5828 ++++++++++- dist/mixpanel.cjs.js | 5828 ++++++++++- dist/mixpanel.globals.js | 274 +- dist/mixpanel.min.js | 212 +- dist/mixpanel.umd.js | 5828 ++++++++++- examples/commonjs-browserify/bundle.js | 5828 ++++++++++- examples/es2015-babelify/bundle.js | 11389 ++++++++++++++++++++- examples/umd-webpack/bundle.js | 5828 ++++++++++- package-lock.json | 39 - src/config.js | 2 +- tests/test.js | 10 +- 16 files changed, 45663 insertions(+), 3306 deletions(-) create mode 100644 dist/mixpanel-main.cjs.js diff --git a/dist/mixpanel-core.cjs.js b/dist/mixpanel-core.cjs.js index ba6f5a8b..dfd0ef23 100644 --- a/dist/mixpanel-core.cjs.js +++ b/dist/mixpanel-core.cjs.js @@ -2,7 +2,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -2051,6 +2051,7 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2075,29 +2076,36 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2108,7 +2116,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2157,61 +2165,67 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2239,25 +2253,32 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2300,7 +2321,10 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2318,7 +2342,8 @@ var RequestBatcher = function(storageKey, options) { this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2335,6 +2360,11 @@ var RequestBatcher = function(storageKey, options) { // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2419,6 +2449,9 @@ RequestBatcher.prototype.flush = function(options) { var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2486,22 +2519,17 @@ RequestBatcher.prototype.flush = function(options) { this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2525,7 +2553,11 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2573,7 +2605,6 @@ RequestBatcher.prototype.flush = function(options) { } logger.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -4206,7 +4237,9 @@ var DEFAULT_CONFIG = { 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4741,7 +4774,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4841,6 +4875,7 @@ MixpanelLib.prototype.init_batchers = function() { attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4852,8 +4887,8 @@ MixpanelLib.prototype.init_batchers = function() { beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); diff --git a/dist/mixpanel-main.cjs.js b/dist/mixpanel-main.cjs.js new file mode 100644 index 00000000..128f89bd --- /dev/null +++ b/dist/mixpanel-main.cjs.js @@ -0,0 +1,6330 @@ +'use strict'; + +var Config = { + DEBUG: false, + LIB_VERSION: '2.53.0' +}; + +/* eslint camelcase: "off", eqeqeq: "off" */ + +// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file +var win; +if (typeof(window) === 'undefined') { + var loc = { + hostname: '' + }; + win = { + navigator: { userAgent: '' }, + document: { + location: loc, + referrer: '' + }, + screen: { width: 0, height: 0 }, + location: loc + }; +} else { + win = window; +} + +// Maximum allowed session recording length +var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours + +/* + * Saved references to long variable names, so that closure compiler can + * minimize file size. + */ + +var ArrayProto = Array.prototype, + FuncProto = Function.prototype, + ObjProto = Object.prototype, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty, + windowConsole = win.console, + navigator = win.navigator, + document$1 = win.document, + windowOpera = win.opera, + screen = win.screen, + userAgent = navigator.userAgent; + +var nativeBind = FuncProto.bind, + nativeForEach = ArrayProto.forEach, + nativeIndexOf = ArrayProto.indexOf, + nativeMap = ArrayProto.map, + nativeIsArray = Array.isArray, + breaker = {}; + +var _ = { + trim: function(str) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } +}; + +// Console override +var console = { + /** @type {function(...*)} */ + log: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } + }, + /** @type {function(...*)} */ + warn: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); + try { + windowConsole.warn.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.warn(arg); + }); + } + } + }, + /** @type {function(...*)} */ + error: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + }, + /** @type {function(...*)} */ + critical: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + } +}; + +var log_func_with_prefix = function(func, prefix) { + return function() { + arguments[0] = '[' + prefix + '] ' + arguments[0]; + return func.apply(console, arguments); + }; +}; +var console_with_prefix = function(prefix) { + return { + log: log_func_with_prefix(console.log, prefix), + error: log_func_with_prefix(console.error, prefix), + critical: log_func_with_prefix(console.critical, prefix) + }; +}; + + +// UNDERSCORE +// Embed part of the Underscore Library +_.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + if (!_.isFunction(func)) { + throw new TypeError(); + } + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) { + return func.apply(context, args.concat(slice.call(arguments))); + } + var ctor = {}; + ctor.prototype = func.prototype; + var self = new ctor(); + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; +}; + +/** + * @param {*=} obj + * @param {function(...*)=} iterator + * @param {Object=} context + */ +_.each = function(obj, iterator, context) { + if (obj === null || obj === undefined) { + return; + } + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { + return; + } + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) { + return; + } + } + } + } +}; + +_.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; +}; + +_.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; +}; + +// from a comment on http://dbj.org/dbj/?p=286 +// fails on only one very rare and deliberate custom object: +// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; +_.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } +}; + +_.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); +}; + +_.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); +}; + +_.map = function(arr, callback, context) { + if (nativeMap && arr.map === nativeMap) { + return arr.map(callback, context); + } else { + var results = []; + _.each(arr, function(item) { + results.push(callback.call(context, item)); + }); + return results; + } +}; + +_.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; +}; + +_.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; +}; + +_.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; +}; + +_.includes = function(str, needle) { + return str.indexOf(needle) !== -1; +}; + +// Underscore Addons +_.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; +}; + +_.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); +}; + +_.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; +}; + +_.isUndefined = function(obj) { + return obj === void 0; +}; + +_.isString = function(obj) { + return toString.call(obj) == '[object String]'; +}; + +_.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; +}; + +_.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; +}; + +_.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); +}; + +_.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; +}; + +_.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); +}; + +_.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); +}; + +_.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; +}; + +/* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ +_.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; +}; + +_.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; +})(); + +/** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ +_.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; +})(); + +_.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; +}; + +_.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; +}; + +_.UUID = (function() { + + // Time-based entropy + var T = function() { + var time = 1 * new Date(); // cross-browser version of Date.now() + var ticks; + if (win.performance && win.performance.now) { + ticks = win.performance.now(); + } else { + // fall back to busy loop + ticks = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (time == 1 * new Date()) { + ticks++; + } + } + return time.toString(16) + Math.floor(ticks).toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; +})(); + +// _.isBlockedUA() +// This is to block various web spiders from executing our JS and +// sending false tracking data +var BLOCKED_UA_STRS = [ + 'ahrefsbot', + 'ahrefssiteaudit', + 'baiduspider', + 'bingbot', + 'bingpreview', + 'chrome-lighthouse', + 'facebookexternal', + 'petalbot', + 'pinterest', + 'screaming frog', + 'yahoo! slurp', + 'yandexbot', + + // a whole bunch of goog-specific crawlers + // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers + 'adsbot-google', + 'apis-google', + 'duplexweb-google', + 'feedfetcher-google', + 'google favicon', + 'google web preview', + 'google-read-aloud', + 'googlebot', + 'googleweblight', + 'mediapartners-google', + 'storebot-google' +]; +_.isBlockedUA = function(ua) { + var i; + ua = ua.toLowerCase(); + for (i = 0; i < BLOCKED_UA_STRS.length; i++) { + if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { + return true; + } + } + return false; +}; + +/** + * @param {Object=} formdata + * @param {string=} arg_separator + */ +_.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); +}; + +_.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + var result = results[1]; + try { + result = decodeURIComponent(result); + } catch(err) { + console.error('Skipping decoding for malformed query param: ' + result); + } + return result.replace(/\+/g, ' '); + } +}; + + +// _.cookie +// Methods partially borrowed from quirksmode.org/js/cookies.html +_.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', + expires = '', + secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { + var cdomain = '', expires = '', secure = ''; + + if (domain_override) { + cdomain = '; domain=' + domain_override; + } else if (is_cross_subdomain) { + var domain = extract_domain(document$1.location.hostname); + cdomain = domain ? '; domain=.' + domain : ''; + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_cross_site) { + is_secure = true; + secure = '; SameSite=None'; + } + if (is_secure) { + secure += '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, is_cross_subdomain, domain_override) { + _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); + } +}; + +var _localStorageSupported = null; +var localStorageSupported = function(storage, forceCheck) { + if (_localStorageSupported !== null && !forceCheck) { + return _localStorageSupported; + } + + var supported = true; + try { + storage = storage || window.localStorage; + var key = '__mplss_' + cheap_guid(8), + val = 'xyz'; + storage.setItem(key, val); + if (storage.getItem(key) !== val) { + supported = false; + } + storage.removeItem(key); + } catch (err) { + supported = false; + } + + _localStorageSupported = supported; + return supported; +}; + +// _.localStorage +_.localStorage = { + is_supported: function(force_check) { + var supported = localStorageSupported(null, force_check); + if (!supported) { + console.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }, + + error: function(msg) { + console.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } +}; + +_.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...*)} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; +})(); + + +var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); + +_.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(TOKEN_MATCH_REGEX); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; +})(); + +var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; +var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; + +_.info = { + campaignParams: function(default_value) { + var kw = '', + params = {}; + _.each(CAMPAIGN_KEYWORDS, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } else if (default_value !== undefined) { + params[kwkey] = default_value; + } + }); + + return params; + }, + + clickParams: function() { + var id = '', + params = {}; + _.each(CLICK_IDS, function(idkey) { + id = _.getQueryParam(document$1.URL, idkey); + if (id.length) { + params[idkey] = id; + } + }); + + return params; + }, + + marketingParams: function() { + return _.extend(_.info.campaignParams(), _.info.clickParams()); + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'SamsungBrowser/')) { + // https://developer.samsung.com/internet/user-agent-string-format + return 'Samsung Internet'; + } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + currentUrl: function() { + return win.location.href; + }, + + properties: function(extra_props) { + if (typeof extra_props !== 'object') { + extra_props = {}; + } + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': _.info.currentUrl(), + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION, + '$insert_id': cheap_guid(), + 'time': _.timestamp() / 1000 // epoch time in seconds + }, _.strip_empty_properties(extra_props)); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) + }); + }, + + mpPageViewProperties: function() { + return _.strip_empty_properties({ + 'current_page_title': document$1.title, + 'current_domain': win.location.hostname, + 'current_url_path': win.location.pathname, + 'current_url_protocol': win.location.protocol, + 'current_url_search': win.location.search + }); + } +}; + +var cheap_guid = function(maxlen) { + var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); + return maxlen ? guid.substring(0, maxlen) : guid; +}; + +// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) +var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; +// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk +var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; +/** + * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For + * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we + * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). + * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy + * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel + * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date + * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK + * offers the 'cookie_domain' config option to set it explicitly. + * @example + * extract_domain('my.sub.example.com') + * // 'example.com' + */ +var extract_domain = function(hostname) { + var domain_regex = DOMAIN_MATCH_REGEX; + var parts = hostname.split('.'); + var tld = parts[parts.length - 1]; + if (tld.length > 4 || tld === 'com' || tld === 'org') { + domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; + } + var matches = hostname.match(domain_regex); + return matches ? matches[0] : ''; +}; + +var JSONStringify = null, JSONParse = null; +if (typeof JSON !== 'undefined') { + JSONStringify = JSON.stringify; + JSONParse = JSON.parse; +} +JSONStringify = JSONStringify || _.JSONEncode; +JSONParse = JSONParse || _.JSONDecode; + +// EXPORTS (for closure compiler) +_['toArray'] = _.toArray; +_['isObject'] = _.isObject; +_['JSONEncode'] = _.JSONEncode; +_['JSONDecode'] = _.JSONDecode; +_['isBlockedUA'] = _.isBlockedUA; +_['isEmptyObject'] = _.isEmptyObject; +_['info'] = _.info; +_['info']['device'] = _.info.device; +_['info']['browser'] = _.info.browser; +_['info']['browserVersion'] = _.info.browserVersion; +_['info']['properties'] = _.info.properties; + +/* eslint camelcase: "off" */ + +/** + * DomTracker Object + * @constructor + */ +var DomTracker = function() {}; + + +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; + +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; + +/** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback + */ +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; +}; + +/** + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; +}; + +DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; + +var logger$2 = console_with_prefix('lock'); + +/** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ +var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; +}; + +// pass in a specific pid to test contention scenarios; otherwise +// it is chosen randomly for each acquisition attempt +SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } +}; + +var logger$1 = console_with_prefix('batch'); + +/** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ +var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; +}; + +/** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ +RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ +RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; +}; + +/** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ +var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; +}; + +/** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ +RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); +}; + +// internal helper for RequestQueue.updatePayloads +var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; +}; + +/** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ +RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); +}; + +/** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ +RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; +}; + +/** + * Serialize the given items array to localStorage. + */ +RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } +}; + +/** + * Clear out queues (memory and localStorage). + */ +RequestQueue.prototype.clear = function() { + this.memQueue = []; + this.storage.removeItem(this.storageKey); +}; + +// maximum interval between request retries after exponential backoff +var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + +var logger = console_with_prefix('batch'); + +/** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ +var RequestBatcher = function(storageKey, options) { + this.errorReporter = options.errorReporter; + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage + }); + + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; + + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; + + this.stopped = !this.libConfig['batch_autostart']; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; +}; + +/** + * Add one item to queue. + */ +RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.flushInterval, cb); +}; + +/** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ +RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); +}; + +/** + * Stop flushing batches. Can be restarted by calling start(). + */ +RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } +}; + +/** + * Clear out queue. + */ +RequestBatcher.prototype.clear = function() { + this.queue.clear(); +}; + +/** + * Restore batch size configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetBatchSize = function() { + this.batchSize = this.libConfig['batch_size']; +}; + +/** + * Restore flush interval time configuration to whatever is set in the main SDK. + */ +RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); +}; + +/** + * Schedule the next flush in the given number of milliseconds. + */ +RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.flushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + } +}; + +/** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ +RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var startTime = new Date().getTime(); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + res.xhr_req && + (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + var headers = res.xhr_req['responseHeaders']; + if (headers) { + var retryAfter = headers['Retry-After']; + if (retryAfter) { + retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; + } + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + this.flush(); // handle next batch if the queue isn't empty + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.stopAllBatching(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger.log('MIXPANEL REQUEST:', dataForRequest); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } +}; + +/** + * Log error to global logger and optional user-defined logger. + */ +RequestBatcher.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + if (this.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.errorReporter(msg, err); + } catch(err) { + logger.error(err); + } + } +}; + +/** + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. + */ + +/** + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + +/** Public **/ + +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} + +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type + */ +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} + +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} + +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} + +/** Private **/ + +/** + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage + */ +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} + +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} + +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} + +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; + + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); + + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; + } + + options = options || {}; + + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} + +/** + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; + + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } + + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +/* eslint camelcase: "off" */ + +/** @const */ var SET_ACTION = '$set'; +/** @const */ var SET_ONCE_ACTION = '$set_once'; +/** @const */ var UNSET_ACTION = '$unset'; +/** @const */ var ADD_ACTION = '$add'; +/** @const */ var APPEND_ACTION = '$append'; +/** @const */ var UNION_ACTION = '$union'; +/** @const */ var REMOVE_ACTION = '$remove'; +/** @const */ var DELETE_ACTION = '$delete'; + +// Common internal methods for mixpanel.people and mixpanel.group APIs. +// These methods shouldn't involve network I/O. +var apiActions = { + set_action: function(prop, to) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + } else { + $set[prop] = to; + } + + data[SET_ACTION] = $set; + return data; + }, + + unset_action: function(prop) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + return data; + }, + + set_once_action: function(prop, to) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return data; + }, + + union_action: function(list_name, values) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + return data; + }, + + append_action: function(list_name, value) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + return data; + }, + + remove_action: function(list_name, value) { + var data = {}; + var $remove = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $remove[k] = v; + } + }, this); + } else { + $remove[list_name] = value; + } + data[REMOVE_ACTION] = $remove; + return data; + }, + + delete_action: function() { + var data = {}; + data[DELETE_ACTION] = ''; + return data; + } +}; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel Group Object + * @constructor + */ +var MixpanelGroup = function() {}; + +_.extend(MixpanelGroup.prototype, apiActions); + +MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { + this._mixpanel = mixpanel_instance; + this._group_key = group_key; + this._group_id = group_id; +}; + +/** + * Set properties on a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Set properties on a group, only if they do not yet exist. + * This will not overwrite previous group property values, unlike + * group.set(). + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); + * + * // or set multiple properties at once + * mixpanel.get_group('company', 'mixpanel').set_once({ + * 'Location': '405 Howard', + * 'Founded' : 2009, + * }); + * // properties can be strings, integers, lists or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/** + * Unset properties on a group permanently. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').unset('Founded'); + * + * @param {String} prop The name of the property. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/** + * Merge a given list with a list-valued group property, excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); + * + * @param {String} list_name Name of the property. + * @param {Array} values Values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/** + * Permanently delete a group. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').delete(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { + // bracket notation above prevents a minification error related to reserved words + var data = this.delete_action(); + return this._send_request(data, callback); +}); + +/** + * Remove a property from a group. The value will be ignored if doesn't exist. + * + * ### Usage: + * + * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); + * + * @param {String} list_name Name of the property. + * @param {Object} value Value to remove from the given group property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ +MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +MixpanelGroup.prototype._send_request = function(data, callback) { + data['$group_key'] = this._group_key; + data['$group_id'] = this._group_id; + data['$token'] = this._get_config('token'); + + var date_encoded_data = _.encodeDates(data); + return this._mixpanel._track_or_batch({ + type: 'groups', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], + batcher: this._mixpanel.request_batchers.groups + }, callback); +}; + +MixpanelGroup.prototype._is_reserved_property = function(prop) { + return prop === '$group_key' || prop === '$group_id'; +}; + +MixpanelGroup.prototype._get_config = function(conf) { + return this._mixpanel.get_config(conf); +}; + +MixpanelGroup.prototype.toString = function() { + return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; +}; + +// MixpanelGroup Exports +MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; +MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; +MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; +MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; +MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; +MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; + +/* eslint camelcase: "off" */ + +/** + * Mixpanel People Object + * @constructor + */ +var MixpanelPeople = function() {}; + +_.extend(MixpanelPeople.prototype, apiActions); + +MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; +}; + +/* +* Set properties on a user record. +* +* ### Usage: +* +* mixpanel.people.set('gender', 'm'); +* +* // or set multiple properties at once +* mixpanel.people.set({ +* 'Company': 'Acme', +* 'Plan': 'Premium', +* 'Upgrade date': new Date() +* }); +* // properties can be strings, integers, dates, or lists +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document.referrer); + } + + // update $set object with default people properties + data[SET_ACTION] = _.extend( + {}, + _.info.people_properties(), + data[SET_ACTION] + ); + return this._send_request(data, callback); +}); + +/* +* Set properties on a user record, only if they do not yet exist. +* This will not overwrite previous people property values, unlike +* people.set(). +* +* ### Usage: +* +* mixpanel.people.set_once('First Login Date', new Date()); +* +* // or set multiple properties at once +* mixpanel.people.set_once({ +* 'First Login Date': new Date(), +* 'Starting Plan': 'Premium' +* }); +* +* // properties can be strings, integers or dates +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [to] A value to set on the given property name +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { + var data = this.set_once_action(prop, to); + if (_.isObject(prop)) { + callback = to; + } + return this._send_request(data, callback); +}); + +/* +* Unset properties on a user record (permanently removes the properties and their values from a profile). +* +* ### Usage: +* +* mixpanel.people.unset('gender'); +* +* // or unset multiple properties at once +* mixpanel.people.unset(['gender', 'Company']); +* +* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { + var data = this.unset_action(prop); + return this._send_request(data, callback); +}); + +/* +* Increment/decrement numeric people analytics properties. +* +* ### Usage: +* +* mixpanel.people.increment('page_views', 1); +* +* // or, for convenience, if you're just incrementing a counter by +* // 1, you can simply do +* mixpanel.people.increment('page_views'); +* +* // to decrement a counter, pass a negative number +* mixpanel.people.increment('credits_left', -1); +* +* // like mixpanel.people.set(), you can increment multiple +* // properties at once: +* mixpanel.people.increment({ +* counter1: 1, +* counter2: 6 +* }); +* +* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. +* @param {Number} [by] An amount to increment the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); +}); + +/* +* Append a value to a list-valued people analytics property. +* +* ### Usage: +* +* // append a value to a list, creating it if needed +* mixpanel.people.append('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.append({ +* list1: 'bob', +* list2: 123 +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value An item to append to the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.append_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Remove a value from a list-valued people analytics property. +* +* ### Usage: +* +* mixpanel.people.remove('School', 'UCB'); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] value Item to remove from the list +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { + if (_.isObject(list_name)) { + callback = value; + } + var data = this.remove_action(list_name, value); + return this._send_request(data, callback); +}); + +/* +* Merge a given list with a list-valued people analytics property, +* excluding duplicate values. +* +* ### Usage: +* +* // merge a value to a list, creating it if needed +* mixpanel.people.union('pages_visited', 'homepage'); +* +* // like mixpanel.people.set(), you can append multiple +* // properties at once: +* mixpanel.people.union({ +* list1: 'bob', +* list2: 123 +* }); +* +* // like mixpanel.people.append(), you can append multiple +* // values to the same list: +* mixpanel.people.union({ +* list1: ['bob', 'billy'] +* }); +* +* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. +* @param {*} [value] Value / values to merge with the given property +* @param {Function} [callback] If provided, the callback will be called after tracking the event. +*/ +MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { + if (_.isObject(list_name)) { + callback = values; + } + var data = this.union_action(list_name, values); + return this._send_request(data, callback); +}); + +/* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + * @deprecated + */ +MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); +}); + +/* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * @deprecated + */ +MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); +}; + +/* +* Permanently deletes the current people analytics profile from +* Mixpanel (using the current distinct_id). +* +* ### Usage: +* +* // remove the all data you have stored about the current user +* mixpanel.people.delete_user(); +* +*/ +MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); +}; + +MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; +}; + +MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + var device_id = this._mixpanel.get_property('$device_id'); + var user_id = this._mixpanel.get_property('$user_id'); + var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); + if (device_id) { + data['$device_id'] = device_id; + } + if (user_id) { + data['$user_id'] = user_id; + } + if (had_persisted_distinct_id) { + data['$had_persisted_distinct_id'] = had_persisted_distinct_id; + } + + var date_encoded_data = _.encodeDates(data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return _.truncate(date_encoded_data, 255); + } + + return this._mixpanel._track_or_batch({ + type: 'people', + data: date_encoded_data, + endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], + batcher: this._mixpanel.request_batchers.people + }, callback); +}; + +MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); +}; + +MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; +}; + +// Queue up engage operations if identify hasn't been called yet. +MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (REMOVE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console.error('Invalid call to _enqueue():', data); + } +}; + +MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + _this._mixpanel['persistence'].save(); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } +}; + +// Flush queued engage operations - order does not matter, +// and there are network level race conditions anyway +MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + var _this = this; + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var append_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); + $append_item = $append_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($append_item)) { + _this.append($append_item, append_callback); + } + } + } + + // same for $remove + var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { + var $remove_item; + var remove_callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); + } + if (!_.isUndefined(_remove_callback)) { + _remove_callback(response, data); + } + }; + for (var j = $remove_queue.length - 1; j >= 0; j--) { + $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); + $remove_item = $remove_queue.pop(); + _this._mixpanel['persistence'].save(); + if (!_.isEmptyObject($remove_item)) { + _this.remove($remove_item, remove_callback); + } + } + } +}; + +MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; +}; + +// MixpanelPeople Exports +MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; +MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; +MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; +MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; +MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; +MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; +MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; +MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; +MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; +MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; +MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + +/* eslint camelcase: "off" */ + +/* + * Constants + */ +/** @const */ var SET_QUEUE_KEY = '__mps'; +/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; +/** @const */ var UNSET_QUEUE_KEY = '__mpus'; +/** @const */ var ADD_QUEUE_KEY = '__mpa'; +/** @const */ var APPEND_QUEUE_KEY = '__mpap'; +/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; +/** @const */ var UNION_QUEUE_KEY = '__mpu'; +// This key is deprecated, but we want to check for it to see whether aliasing is allowed. +/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; +/** @const */ var ALIAS_ID_KEY = '__alias'; +/** @const */ var EVENT_TIMERS_KEY = '__timers'; +/** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + REMOVE_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + EVENT_TIMERS_KEY +]; + +/** + * Mixpanel Persistence Object + * @constructor + */ +var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + if (storage_type === 'localStorage' && _.localStorage.is_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(); + this.save(); +}; + +MixpanelPersistence.prototype.properties = function() { + var p = {}; + + this.load(); + + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; +}; + +MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } +}; + +MixpanelPersistence.prototype.upgrade = function() { + var old_cookie, + old_localstorage; + + // if transferring from cookie to localStorage or vice-versa, copy existing + // super properties over to new storage mode + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } else if (this.storage === _.cookie) { + old_localstorage = _.localStorage.parse(this.name); + + _.localStorage.remove(this.name); + + if (old_localstorage) { + this.register_once(old_localstorage); + } + } +}; + +MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure, + this.cross_site, + this.cookie_domain + ); +}; + +MixpanelPersistence.prototype.load_prop = function(key) { + this.load(); + return this['props'][key]; +}; + +MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false, this.cookie_domain); + this.storage.remove(this.name, true, this.cookie_domain); +}; + +// removes the storage entry and deletes all loaded data +// forced name for tests +MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; +}; + +/** +* @param {Object} props +* @param {*=} default_value +* @param {number=} days +*/ +MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; +}; + +/** +* @param {Object} props +* @param {number=} days +*/ +MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + this.load(); + _.extend(this['props'], props); + this.save(); + + return true; + } + return false; +}; + +MixpanelPersistence.prototype.unregister = function(prop) { + this.load(); + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } +}; + +MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); +}; + +// EXPORTED METHOD, we test this directly. +MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); +}; + +MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); +}; + +MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cookie_domain(config['cookie_domain']); + this.set_cross_site(config['cross_site_cookie']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); +}; + +MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } else { + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { + if (cookie_domain !== this.cookie_domain) { + this.remove(); + this.cookie_domain = cookie_domain; + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_site = function(cross_site) { + if (cross_site !== this.cross_site) { + this.cross_site = cross_site; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; +}; + +MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } +}; + +MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + remove_q = this._get_or_create_queue(REMOVE_ACTION, []), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === REMOVE_QUEUE_KEY) { + remove_q.push(q_data); + this._pop_from_people_queue(APPEND_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console.log(data); + + this.save(); +}; + +MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this['props'][this._get_queue_key(queue)]; + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { + // list actions: only remove if both k+v match + // e.g. remove should not override append in a case like + // append({foo: 'bar'}); remove({foo: 'qux'}) + _.each(q, function(queued_action) { + if (queued_action[k] === v) { + delete queued_action[k]; + } + }); + } else { + delete q[k]; + } + }, this); + } +}; + +MixpanelPersistence.prototype.load_queue = function(queue) { + return this.load_prop(this._get_queue_key(queue)); +}; + +MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === REMOVE_ACTION) { + return REMOVE_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console.error('Invalid queue:', queue); + } +}; + +MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + return this['props'][key] || (this['props'][key] = default_val); +}; + +MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); +}; + +MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; +}; + +/* eslint camelcase: "off" */ + +/* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @output_file_name mixpanel-2.8.min.js +// ==/ClosureCompiler== + +/* +SIMPLE STYLE GUIDE: + +this.x === public function +this._x === internal - only use within this file +this.__x === private - only use within the class + +Globals should be all caps +*/ + +var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + +var mixpanel_master; // main mixpanel instance / object +var INIT_MODULE = 0; +var INIT_SNIPPET = 1; + +var IDENTITY_FUNC = function(x) {return x;}; +var NOOP_FUNC = function() {}; + +/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; +/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; +/** @const */ var PAYLOAD_TYPE_JSON = 'json'; +/** @const */ var DEVICE_ID_PREFIX = '$device:'; + + +/* + * Dynamic... constants? Is that an oxymoron? + */ +// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ +// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials +var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + +// IE<10 does not support cross-origin XHR's but script tags +// with defer won't block window.onload; ENQUEUE_REQUESTS +// should only be true for Opera<12 +var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + +// save reference to navigator.sendBeacon so it can be minified +var sendBeacon = null; +if (navigator['sendBeacon']) { + sendBeacon = function() { + // late reference to navigator.sendBeacon to allow patching/spying + return navigator['sendBeacon'].apply(navigator, arguments); + }; +} + +var DEFAULT_API_ROUTES = { + 'track': 'track/', + 'engage': 'engage/', + 'groups': 'groups/', + 'record': 'record/' +}; + +/* + * Module-level globals + */ +var DEFAULT_CONFIG = { + 'api_host': 'https://api-js.mixpanel.com', + 'api_routes': DEFAULT_API_ROUTES, + 'api_method': 'POST', + 'api_transport': 'XHR', + 'api_payload_format': PAYLOAD_TYPE_BASE64, + 'app_host': 'https://mixpanel.com', + 'cdn': 'https://cdn.mxpnl.com', + 'cross_site_cookie': false, + 'cross_subdomain_cookie': true, + 'error_reporter': NOOP_FUNC, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_domain': '', + 'cookie_name': '', + 'loaded': NOOP_FUNC, + 'mp_loader': null, + 'track_marketing': true, + 'track_pageview': false, + 'skip_first_touch_marketing': false, + 'store_google': true, + 'stop_utm_persistence': false, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'opt_out_tracking_by_default': false, + 'opt_out_persistence_by_default': false, + 'opt_out_tracking_persistence_type': 'localStorage', + 'opt_out_tracking_cookie_prefix': null, + 'property_blacklist': [], + 'xhr_headers': {}, // { header: value, header2: value } + 'ignore_dnt': false, + 'batch_requests': true, + 'batch_size': 50, + 'batch_flush_interval_ms': 5000, + 'batch_request_timeout_ms': 90000, + 'batch_autostart': true, + 'hooks': {}, + 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), + 'record_block_selector': 'img, video', + 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), + 'record_mask_text_selector': '*', + 'record_max_ms': MAX_RECORDING_MS, + 'record_sessions_percent': 0, + 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' +}; + +var DOM_LOADED = false; + +/** + * Mixpanel Library Object + * @constructor + */ +var MixpanelLib = function() {}; + + +/** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ +var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._cached_groups = {}; // cache groups in a pool + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + if (!instance.get_config('skip_first_touch_marketing')) { + // We need null UTM params in the object because + // UTM parameters act as a tuple. If any UTM param + // is present, then we set all UTM params including + // empty ones together + var utm_params = _.info.campaignParams(null); + var initial_utm_params = {}; + var has_utm = false; + _.each(utm_params, function(utm_value, utm_key) { + initial_utm_params['initial_' + utm_key] = utm_value; + if (utm_value) { + has_utm = true; + } + }); + if (has_utm) { + instance['people'].set_once(initial_utm_params); + } + } + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; +}; + +// Initialization methods + +/** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ +MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + this.report_error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; +}; + +// mixpanel._init(token:string, config:object, name:string) +// +// This function sets up the current instance of the mixpanel +// library. The difference between this method and the init(...) +// method is this one initializes the actual instance, whereas the +// init(...) method sets up a new library and calls _init on it. +// +MixpanelLib.prototype._init = function(token, config, name) { + config = config || {}; + + this['__loaded'] = true; + this['config'] = {}; + + var variable_features = {}; + + // default to JSON payload for standard mixpanel.com API hosts + if (!('api_payload_format' in config)) { + var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; + if (api_host.match(/\.mixpanel\.com/)) { + variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; + } + } + + this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = NOOP_FUNC; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + // set up request queueing/batching + this.request_batchers = {}; + this._batch_requests = this.get_config('batch_requests'); + if (this._batch_requests) { + if (!_.localStorage.is_supported(true) || !USE_XHR) { + this._batch_requests = false; + console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + _.each(this.get_batcher_configs(), function(batcher_config) { + console.log('Clearing batch queue ' + batcher_config.queue_key); + _.localStorage.remove(batcher_config.queue_key); + }); + } else { + this.init_batchers(); + if (sendBeacon && win.addEventListener) { + // Before page closes or hides (user tabs away etc), attempt to flush any events + // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, + // events will not be removed from the persistent store; if the site is loaded again, + // the events will be flushed again on startup and deduplicated on the Mixpanel server + // side. + // There is no reliable way to capture only page close events, so we lean on the + // visibilitychange and pagehide events as recommended at + // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. + // These events fire when the user clicks away from the current page/tab, so will occur + // more frequently than page unload, but are the only mechanism currently for capturing + // this scenario somewhat reliably. + var flush_on_unload = _.bind(function() { + if (!this.request_batchers.events.stopped) { + this.request_batchers.events.flush({unloading: true}); + } + }, this); + win.addEventListener('pagehide', function(ev) { + if (ev['persisted']) { + flush_on_unload(); + } + }); + win.addEventListener('visibilitychange', function() { + if (document$1['visibilityState'] === 'hidden') { + flush_on_unload(); + } + }); + } + } + } + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.unpersisted_superprops = {}; + this._gdpr_init(); + + var uuid = _.UUID(); + if (!this.get_distinct_id()) { + // There is no need to set the distinct id + // or the device id if something was already stored + // in the persitence + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); + } + + var track_pageview_option = this.get_config('track_pageview'); + if (track_pageview_option) { + this._init_url_change_tracking(track_pageview_option); + } + + if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { + this.start_session_recording(); + } +}; + +MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { + if (!win['MutationObserver']) { + console.critical('Browser does not support MutationObserver; skipping session recording'); + return; + } + + var handleLoadedRecorder = _.bind(function() { + this._recorder = this._recorder || new win['__mp_recorder'](this); + this._recorder['startRecording'](); + }, this); + + if (_.isUndefined(win['__mp_recorder'])) { + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); + } else { + handleLoadedRecorder(); + } +}); + +MixpanelLib.prototype.stop_session_recording = function () { + if (this._recorder) { + this._recorder['stopRecording'](); + } else { + console.critical('Session recorder module not loaded'); + } +}; + +MixpanelLib.prototype.get_session_recording_properties = function () { + var props = {}; + if (this._recorder) { + var replay_id = this._recorder['replayId']; + if (replay_id) { + props['$mp_replay_id'] = replay_id; + } + } + return props; +}; + +// Private methods + +MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + this._set_default_superprops(); + this['people'].set_once(this['persistence'].get_referrer_info()); + + // `store_google` is now deprecated and previously stored UTM parameters are cleared + // from persistence by default. + if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { + var utm_params = _.info.campaignParams(null); + _.each(utm_params, function(_utm_value, utm_key) { + // We need to unregister persisted UTM parameters so old values + // are not mixed with the new UTM parameters + this.unregister(utm_key); + }.bind(this)); + } +}; + +// update persistence with info on referrer, UTM params, etc +MixpanelLib.prototype._set_default_superprops = function() { + this['persistence'].update_search_keyword(document$1.referrer); + // Registering super properties for UTM persistence by 'store_google' is deprecated. + if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { + this.register(_.info.campaignParams()); + } + if (this.get_config('save_referrer')) { + this['persistence'].update_referrer_info(document$1.referrer); + } +}; + +MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + + if (!this.has_opted_out_tracking()) { + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + } + + delete this.__dom_loaded_queue; + delete this.__request_queue; +}; + +MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + this.report_error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); +}; + +MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { + var previous_tracked_url = ''; + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = _.info.currentUrl(); + } + + if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { + win.addEventListener('popstate', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + win.addEventListener('hashchange', function() { + win.dispatchEvent(new Event('mp_locationchange')); + }); + var nativePushState = win.history.pushState; + if (typeof nativePushState === 'function') { + win.history.pushState = function(state, unused, url) { + nativePushState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + var nativeReplaceState = win.history.replaceState; + if (typeof nativeReplaceState === 'function') { + win.history.replaceState = function(state, unused, url) { + nativeReplaceState.call(win.history, state, unused, url); + win.dispatchEvent(new Event('mp_locationchange')); + }; + } + win.addEventListener('mp_locationchange', function() { + var current_url = _.info.currentUrl(); + var should_track = false; + if (track_pageview_option === 'full-url') { + should_track = current_url !== previous_tracked_url; + } else if (track_pageview_option === 'url-with-path-and-query-string') { + should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; + } else if (track_pageview_option === 'url-with-path') { + should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; + } + + if (should_track) { + var tracked = this.track_pageview(); + if (tracked) { + previous_tracked_url = current_url; + } + } + }.bind(this)); + } +}; + +/** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ +MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } +}; + +MixpanelLib.prototype._send_request = function(url, data, options, callback) { + var succeeded = true; + + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return succeeded; + } + + var DEFAULT_OPTIONS = { + method: this.get_config('api_method'), + transport: this.get_config('api_transport'), + verbose: this.get_config('verbose') + }; + var body_data = null; + + if (!callback && (_.isFunction(options) || typeof options === 'string')) { + callback = options; + options = null; + } + options = _.extend(DEFAULT_OPTIONS, options || {}); + if (!USE_XHR) { + options.method = 'GET'; + } + var use_post = options.method === 'POST'; + var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; + + // needed to correctly format responses + var verbose_mode = options.verbose; + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + + if (use_post) { + body_data = 'data=' + encodeURIComponent(data['data']); + delete data['data']; + } + + url += '?' + _.HTTPBuildQuery(data); + + var lib = this; + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (use_sendBeacon) { + try { + succeeded = sendBeacon(url, body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + try { + if (callback) { + callback(succeeded ? 1 : 0); + } + } catch (e) { + lib.report_error(e); + } + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open(options.method, url, true); + + var headers = this.get_config('xhr_headers'); + if (use_post) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + _.each(headers, function(headerValue, headerName) { + req.setRequestHeader(headerName, headerValue); + }); + + if (options.timeout_ms && typeof req.timeout !== 'undefined') { + req.timeout = options.timeout_ms; + var start_time = new Date().getTime(); + } + + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + lib.report_error(e); + if (options.ignore_json_errors) { + response = req.responseText; + } else { + return; + } + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error; + if ( + req.timeout && + !req.status && + new Date().getTime() - start_time >= req.timeout + ) { + error = 'timeout'; + } else { + error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + } + lib.report_error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error, xhr_req: req}); + } else { + callback(0); + } + } + } + } + }; + req.send(body_data); + } catch (e) { + lib.report_error(e); + succeeded = false; + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + + return succeeded; +}; + +/** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ +MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (_.isArray(fn_name)) { + tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() + } else if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + if (_.isArray(item[0])) { + // chained call + var caller = context; + _.each(item, function(call) { + caller = caller[call[0]].apply(caller, call.slice(1)); + }); + } else { + this[item[0]].apply(this, item.slice(1)); + } + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); +}; + +// request queueing utils + +MixpanelLib.prototype.are_batchers_initialized = function() { + return !!this.request_batchers.events; +}; + +MixpanelLib.prototype.get_batcher_configs = function() { + var queue_prefix = '__mpq_' + this.get_config('token'); + var api_routes = this.get_config('api_routes'); + this._batcher_configs = this._batcher_configs || { + events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, + people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, + groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} + }; + return this._batcher_configs; +}; + +MixpanelLib.prototype.init_batchers = function() { + if (!this.are_batchers_initialized()) { + var batcher_for = _.bind(function(attrs) { + return new RequestBatcher( + attrs.queue_key, + { + libConfig: this['config'], + sendRequestFunc: _.bind(function(data, options, cb) { + this._send_request( + this.get_config('api_host') + attrs.endpoint, + this._encode_data_for_request(data), + options, + this._prepare_callback(cb, data) + ); + }, this), + beforeSendHook: _.bind(function(item) { + return this._run_hook('before_send_' + attrs.type, item); + }, this), + errorReporter: this.get_config('error_reporter'), + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + } + ); + }, this); + var batcher_configs = this.get_batcher_configs(); + this.request_batchers = { + events: batcher_for(batcher_configs.events), + people: batcher_for(batcher_configs.people), + groups: batcher_for(batcher_configs.groups) + }; + } + if (this.get_config('batch_autostart')) { + this.start_batch_senders(); + } +}; + +MixpanelLib.prototype.start_batch_senders = function() { + this._batchers_were_started = true; + if (this.are_batchers_initialized()) { + this._batch_requests = true; + _.each(this.request_batchers, function(batcher) { + batcher.start(); + }); + } +}; + +MixpanelLib.prototype.stop_batch_senders = function() { + this._batch_requests = false; + _.each(this.request_batchers, function(batcher) { + batcher.stop(); + batcher.clear(); + }); +}; + +/** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ +MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); +}; + +/** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ +MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } +}; + +MixpanelLib.prototype._encode_data_for_request = function(data) { + var encoded_data = _.JSONEncode(data); + if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { + encoded_data = _.base64Encode(encoded_data); + } + return {'data': encoded_data}; +}; + +// internal method for handling track vs batch-enqueue logic +MixpanelLib.prototype._track_or_batch = function(options, callback) { + var truncated_data = _.truncate(options.data, 255); + var endpoint = options.endpoint; + var batcher = options.batcher; + var should_send_immediately = options.should_send_immediately; + var send_request_options = options.send_request_options || {}; + callback = callback || NOOP_FUNC; + + var request_enqueued_or_initiated = true; + var send_request_immediately = _.bind(function() { + if (!send_request_options.skip_hooks) { + truncated_data = this._run_hook('before_send_' + options.type, truncated_data); + } + if (truncated_data) { + console.log('MIXPANEL REQUEST:'); + console.log(truncated_data); + return this._send_request( + endpoint, + this._encode_data_for_request(truncated_data), + send_request_options, + this._prepare_callback(callback, truncated_data) + ); + } else { + return null; + } + }, this); + + if (this._batch_requests && !should_send_immediately) { + batcher.enqueue(truncated_data, function(succeeded) { + if (succeeded) { + callback(1, truncated_data); + } else { + send_request_immediately(); + } + }); + } else { + request_enqueued_or_initiated = send_request_immediately(); + } + + return request_enqueued_or_initiated && truncated_data; +}; + +/** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * // track an event using navigator.sendBeacon + * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Object} [options] Optional configuration for this track request. + * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). + * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + var transport = options['transport']; // external API, don't minify 'transport' prop + if (transport) { + options.transport = transport; // 'transport' prop name can be minified internally + } + var should_send_immediately = options['send_immediately']; + if (typeof callback !== 'function') { + callback = NOOP_FUNC; + } + + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = _.extend({}, properties); + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + this._set_default_superprops(); + + var marketing_properties = this.get_config('track_marketing') + ? _.info.marketingParams() + : {}; + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties({'mp_loader': this.get_config('mp_loader')}), + marketing_properties, + this['persistence'].properties(), + this.unpersisted_superprops, + this.get_session_recording_properties(), + properties + ); + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + var ret = this._track_or_batch({ + type: 'events', + data: data, + endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], + batcher: this.request_batchers.events, + should_send_immediately: should_send_immediately, + send_request_options: options + }, callback); + + return ret; +}); + +/** + * Register the current user into one/many groups. + * + * ### Usage: + * + * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs + * mixpanel.set_group('company', 'mixpanel') + * mixpanel.set_group('company', 128746312) + * + * @param {String} group_key Group key + * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + * + */ +MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { + if (!_.isArray(group_ids)) { + group_ids = [group_ids]; + } + var prop = {}; + prop[group_key] = group_ids; + this.register(prop); + return this['people'].set(group_key, group_ids, callback); +}); + +/** + * Add a new group for this user. + * + * ### Usage: + * + * mixpanel.add_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_values = this.get_property(group_key); + var prop = {}; + if (old_values === undefined) { + prop[group_key] = [group_id]; + this.register(prop); + } else { + if (old_values.indexOf(group_id) === -1) { + old_values.push(group_id); + prop[group_key] = old_values; + this.register(prop); + } + } + return this['people'].union(group_key, group_id, callback); +}); + +/** + * Remove a group from this user. + * + * ### Usage: + * + * mixpanel.remove_group('company', 'mixpanel') + * + * @param {String} group_key Group key + * @param {*} group_id A valid Mixpanel property type + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { + var old_value = this.get_property(group_key); + // if the value doesn't exist, the persistent store is unchanged + if (old_value !== undefined) { + var idx = old_value.indexOf(group_id); + if (idx > -1) { + old_value.splice(idx, 1); + this.register({group_key: old_value}); + } + if (old_value.length === 0) { + this.unregister(group_key); + } + } + return this['people'].remove(group_key, group_id, callback); +}); + +/** + * Track an event with specific groups. + * + * ### Usage: + * + * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) + * + * @param {String} event_name The name of the event (see `mixpanel.track()`) + * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) + * @param {Object=} groups An object mapping group name keys to one or more values + * @param {Function} [callback] If provided, the callback will be called after tracking the event. + */ +MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { + var tracking_props = _.extend({}, properties || {}); + _.each(groups, function(v, k) { + if (v !== null && v !== undefined) { + tracking_props[k] = v; + } + }); + return this.track(event_name, tracking_props, callback); +}); + +MixpanelLib.prototype._create_map_key = function (group_key, group_id) { + return group_key + '_' + JSON.stringify(group_id); +}; + +MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { + delete this._cached_groups[this._create_map_key(group_key, group_id)]; +}; + +/** + * Look up reference to a Mixpanel group + * + * ### Usage: + * + * mixpanel.get_group(group_key, group_id) + * + * @param {String} group_key Group key + * @param {Object} group_id A valid Mixpanel property type + * @returns {Object} A MixpanelGroup identifier + */ +MixpanelLib.prototype.get_group = function (group_key, group_id) { + var map_key = this._create_map_key(group_key, group_id); + var group = this._cached_groups[map_key]; + if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { + group = new MixpanelGroup(); + group._init(this, group_key, group_id); + this._cached_groups[map_key] = group; + } + return group; +}; + +/** + * Track a default Mixpanel page view event, which includes extra default event properties to + * improve page view data. + * + * ### Usage: + * + * // track a default $mp_web_page_view event + * mixpanel.track_pageview(); + * + * // track a page view event with additional event properties + * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); + * + * // example approach to track page views on different page types as event properties + * mixpanel.track_pageview({'page': 'pricing'}); + * mixpanel.track_pageview({'page': 'homepage'}); + * + * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for + * // individual pages on the same site or product. Use cases for custom event_name may be page + * // views on different products or internal applications that are considered completely separate + * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); + * + * ### Notes: + * + * The `config.track_pageview` option for mixpanel.init() + * may be turned on for tracking page loads automatically. + * + * // track only page loads + * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); + * + * // track when the URL changes in any manner + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); + * + * // track when the URL changes, ignoring any changes in the hash part + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); + * + * // track when the path changes, ignoring any query parameter or hash changes + * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); + * + * @param {Object} [properties] An optional set of additional properties to send with the page view event + * @param {Object} [options] Page view tracking options + * @param {String} [options.event_name] - Alternate name for the tracking event + * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object + * with the tracking payload sent to the API server is returned; otherwise false. + */ +MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { + if (typeof properties !== 'object') { + properties = {}; + } + options = options || {}; + var event_name = options['event_name'] || '$mp_web_page_view'; + + var default_page_properties = _.extend( + _.info.mpPageViewProperties(), + _.info.campaignParams(), + _.info.clickParams() + ); + + var event_properties = _.extend( + {}, + default_page_properties, + properties + ); + + return this.track(event_name, event_properties); +}); + +/** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ +MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); +}; + +/** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ +MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); +}; + +/** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ +MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + this.report_error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); +}; + +var REGISTER_DEFAULTS = { + 'persistent': true +}; +/** + * Helper to parse options param for register methods, maintaining + * legacy support for plain "days" param instead of options object + * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods + * @returns {Object} options object + */ +var options_for_register = function(days_or_options) { + var options; + if (_.isObject(days_or_options)) { + options = days_or_options; + } else if (!_.isUndefined(days_or_options)) { + options = {'days': days_or_options}; + } else { + options = {}; + } + return _.extend({}, REGISTER_DEFAULTS, options); +}; + +/** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * // register only for the current pageload + * mixpanel.register({'Name': 'Pat'}, {persistent: false}); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register = function(props, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register(props, options['days']); + } else { + _.extend(this.unpersisted_superprops, props); + } +}; + +/** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * // register once, only for the current pageload + * mixpanel.register_once({ + * 'First interaction time': new Date().toISOString() + * }, 'None', {persistent: false}); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) + * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { + var options = options_for_register(days_or_options); + if (options['persistent']) { + this['persistence'].register_once(props, default_value, options['days']); + } else { + if (typeof(default_value) === 'undefined') { + default_value = 'None'; + } + _.each(props, function(val, prop) { + if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { + this.unpersisted_superprops[prop] = val; + } + }, this); + } +}; + +/** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + * @param {Object} [options] + * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) + */ +MixpanelLib.prototype.unregister = function(property, options) { + options = options_for_register(options); + if (options['persistent']) { + this['persistence'].unregister(property); + } else { + delete this.unpersisted_superprops[property]; + } +}; + +MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); +}; + +/** + * Identify a user with a unique ID to track user activity across + * devices, tie a user to their events, and create a user profile. + * If you never call this method, unique visitors are tracked using + * a UUID generated the first time they visit the site. + * + * Call identify when you know the identity of the current user, + * typically after login or signup. We recommend against using + * identify for anonymous visitors to your site. + * + * ### Notes: + * If your project has + * ID Merge + * enabled, the identify method will connect pre- and + * post-authentication events when appropriate. + * + * If your project does not have ID Merge enabled, identify will + * change the user's local distinct_id to the unique ID you pass. + * Events tracked prior to authentication will not be connected + * to the same user identity. If ID Merge is disabled, alias can + * be used to connect pre- and post-registration events. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ +MixpanelLib.prototype.identify = function( + new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback +) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + var previous_distinct_id = this.get_distinct_id(); + if (new_distinct_id && previous_distinct_id !== new_distinct_id) { + // we allow the following condition if previous distinct_id is same as new_distinct_id + // so that you can force flush people updates for anonymous profiles. + if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { + this.report_error('distinct_id cannot have $device: prefix'); + return -1; + } + this.register({'$user_id': new_distinct_id}); + } + + if (!this.get_property('$device_id')) { + // The persisted distinct id might not actually be a device id at all + // it might be a distinct id of the user from before + var device_id = previous_distinct_id; + this.register_once({ + '$had_persisted_distinct_id': true, + '$device_id': device_id + }, ''); + } + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this.register({'distinct_id': new_distinct_id}); + } + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); + + // send an $identify event any time the distinct_id is changing - logic on the server + // will determine whether or not to do anything with it. + if (new_distinct_id !== previous_distinct_id) { + this.track('$identify', { + 'distinct_id': new_distinct_id, + '$anon_distinct_id': previous_distinct_id + }, {skip_hooks: true}); + } +}; + +/** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ +MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + var uuid = _.UUID(); + this.register_once({ + 'distinct_id': DEVICE_ID_PREFIX + uuid, + '$device_id': uuid + }, ''); +}; + +/** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ +MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); +}; + +/** + * The alias method creates an alias which Mixpanel will use to + * remap one id to another. Multiple aliases can point to the + * same identifier. + * + * The following is a valid use of alias: + * + * mixpanel.alias('new_id', 'existing_id'); + * // You can add multiple id aliases to the existing ID + * mixpanel.alias('newer_id', 'existing_id'); + * + * Aliases can also be chained - the following is a valid example: + * + * mixpanel.alias('new_id', 'existing_id'); + * // chain newer_id - new_id - existing_id + * mixpanel.alias('newer_id', 'new_id'); + * + * Aliases cannot point to multiple identifiers - the following + * example will not work: + * + * mixpanel.alias('new_id', 'existing_id'); + * // this is invalid as 'new_id' already points to 'existing_id' + * mixpanel.alias('new_id', 'newer_id'); + * + * ### Notes: + * + * If your project does not have + * ID Merge + * enabled, the best practice is to call alias once when a unique + * ID is first created for a user (e.g., when a user first registers + * for an account). Do not use alias multiple times for a single + * user without ID Merge enabled. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ +MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + this.report_error('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { + 'alias': alias, + 'distinct_id': original + }, { + skip_hooks: true + }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + this.report_error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } +}; + +/** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @deprecated + */ +MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); +}; + +/** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // host for requests (customizable for e.g. a local proxy) + * api_host: 'https://api-js.mixpanel.com', + * + * // endpoints for different types of requests + * api_routes: { + * track: 'track/', + * engage: 'engage/', + * groups: 'groups/', + * } + * + * // HTTP method for tracking requests + * api_method: 'POST' + * + * // transport for sending requests ('XHR' or 'sendBeacon') + * // NB: sendBeacon should only be used for scenarios such as + * // page unload where a "best-effort" attempt to send is + * // acceptable; the sendBeacon API does not support callbacks + * // or any way to know the result of the request. Mixpanel + * // tracking via sendBeacon will not support any event- + * // batching or retry mechanisms. + * api_transport: 'XHR' + * + * // request-batching/queueing/retry + * batch_requests: true, + * + * // maximum number of events/updates to send in a single + * // network request + * batch_size: 50, + * + * // milliseconds to wait between sending batch requests + * batch_flush_interval_ms: 5000, + * + * // milliseconds to wait for network responses to batch requests + * // before they are considered timed-out and retried + * batch_request_timeout_ms: 90000, + * + * // override value for cookie domain, only useful for ensuring + * // correct cross-subdomain cookies on unusual domains like + * // subdomain.mainsite.avocat.fr; NB this cannot be used to + * // set cookies on a different domain than the current origin + * cookie_domain: '' + * + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // if true, cookie will be set with SameSite=None; Secure + * // this is only useful in special situations, like embedded + * // 3rd-party iframes that set up a Mixpanel instance + * cross_site_cookie: false + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // opt users out of tracking by this Mixpanel instance by default + * opt_out_tracking_by_default: false + * + * // opt users out of browser data storage by this Mixpanel instance by default + * opt_out_persistence_by_default: false + * + * // persistence mechanism used by opt-in/opt-out methods - cookie + * // or localStorage - falls back to cookie if localStorage is unavailable + * opt_out_tracking_persistence_type: 'localStorage' + * + * // customize the name of cookie/localStorage set by opt-in/opt-out methods + * opt_out_tracking_cookie_prefix: null + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // disables enriching user profiles with first touch marketing data + * skip_first_touch_marketing: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // adds any UTM parameters and click IDs present on the page to any events fired + * track_marketing: true + * + * // enables automatic page view tracking using default page view events through + * // the track_pageview() method + * track_pageview: false + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * + * // extra HTTP request headers to set for each API request, in + * // the format {'Header-Name': value} + * xhr_headers: {} + * + * // whether to ignore or respect the web browser's Do Not Track setting + * ignore_dnt: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ +MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + var new_batch_size = config['batch_size']; + if (new_batch_size) { + _.each(this.request_batchers, function(batcher) { + batcher.resetBatchSize(); + }); + } + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } +}; + +/** + * returns the current config object for the library. + */ +MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; +}; + +/** + * Fetch a hook function from config, with safe default, and run it + * against the given arguments + * @param {string} hook_name which hook to retrieve + * @returns {any|null} return value of user-provided hook, or null if nothing was returned + */ +MixpanelLib.prototype._run_hook = function(hook_name) { + var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); + if (typeof ret === 'undefined') { + this.report_error(hook_name + ' hook did not return a value'); + ret = null; + } + return ret; +}; + +/** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ +MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence'].load_prop([property_name]); +}; + +MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; +}; + +MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); +}; + +// perform some housekeeping around GDPR opt-in/out state +MixpanelLib.prototype._gdpr_init = function() { + var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; + + // try to convert opt-in/out cookies to localStorage if possible + if (is_localStorage_requested && _.localStorage.is_supported()) { + if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { + this.opt_in_tracking({'enable_persistence': false}); + } + if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { + this.opt_out_tracking({'clear_persistence': false}); + } + this.clear_opt_in_out_tracking({ + 'persistence_type': 'cookie', + 'enable_persistence': false + }); + } + + // check whether the user has already opted out - if so, clear & disable persistence + if (this.has_opted_out_tracking()) { + this._gdpr_update_persistence({'clear_persistence': true}); + + // check whether we should opt out by default + // note: we don't clear persistence here by default since opt-out default state is often + // used as an initial state while GDPR information is being collected + } else if (!this.has_opted_in_tracking() && ( + this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') + )) { + _.cookie.remove('mp_optout'); + this.opt_out_tracking({ + 'clear_persistence': this.get_config('opt_out_persistence_by_default') + }); + } +}; + +/** + * Enable or disable persistence based on options + * only enable/disable if persistence is not already in this state + * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it + * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence + */ +MixpanelLib.prototype._gdpr_update_persistence = function(options) { + var disabled; + if (options && options['clear_persistence']) { + disabled = true; + } else if (options && options['enable_persistence']) { + disabled = false; + } else { + return; + } + + if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { + this['persistence'].set_disabled(disabled); + } + + if (disabled) { + this.stop_batch_senders(); + } else { + // only start batchers after opt-in if they have previously been started + // in order to avoid unintentionally starting up batching for the first time + if (this._batchers_were_started) { + this.start_batch_senders(); + } + } +}; + +// call a base gdpr function after constructing the appropriate token and options args +MixpanelLib.prototype._gdpr_call_func = function(func, options) { + options = _.extend({ + 'track': _.bind(this.track, this), + 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), + 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), + 'cookie_expiration': this.get_config('cookie_expiration'), + 'cross_site_cookie': this.get_config('cross_site_cookie'), + 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), + 'cookie_domain': this.get_config('cookie_domain'), + 'secure_cookie': this.get_config('secure_cookie'), + 'ignore_dnt': this.get_config('ignore_dnt') + }, options); + + // check if localStorage can be used for recording opt out status, fall back to cookie if not + if (!_.localStorage.is_supported()) { + options['persistence_type'] = 'cookie'; + } + + return func(this.get_config('token'), { + track: options['track'], + trackEventName: options['track_event_name'], + trackProperties: options['track_properties'], + persistenceType: options['persistence_type'], + persistencePrefix: options['cookie_prefix'], + cookieDomain: options['cookie_domain'], + cookieExpiration: options['cookie_expiration'], + crossSiteCookie: options['cross_site_cookie'], + crossSubdomainCookie: options['cross_subdomain_cookie'], + secureCookie: options['secure_cookie'], + ignoreDnt: options['ignore_dnt'] + }); +}; + +/** + * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user in + * mixpanel.opt_in_tracking(); + * + * // opt user in with specific event name, properties, cookie configuration + * mixpanel.opt_in_tracking({ + * track_event_name: 'User opted in', + * track_event_properties: { + * 'Email': 'jdoe@example.com' + * }, + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) + * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action + * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_in_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(optIn, options); + this._gdpr_update_persistence(options); +}; + +/** + * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // opt user out + * mixpanel.opt_out_tracking(); + * + * // opt user out with different cookie configuration from Mixpanel instance + * mixpanel.opt_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out + * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.opt_out_tracking = function(options) { + options = _.extend({ + 'clear_persistence': true, + 'delete_user': true + }, options); + + // delete user and clear charges since these methods may be disabled by opt-out + if (options['delete_user'] && this['people'] && this['people']._identify_called()) { + this['people'].delete_user(); + this['people'].clear_charges(); + } + + this._gdpr_call_func(optOut, options); + this._gdpr_update_persistence(options); +}; + +/** + * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_in = mixpanel.has_opted_in_tracking(); + * // use has_opted_in value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-in status + */ +MixpanelLib.prototype.has_opted_in_tracking = function(options) { + return this._gdpr_call_func(hasOptedIn, options); +}; + +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * var has_opted_out = mixpanel.has_opted_out_tracking(); + * // use has_opted_out value + * + * @param {Object} [options] A dictionary of config options to override + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @returns {boolean} current opt-out status + */ +MixpanelLib.prototype.has_opted_out_tracking = function(options) { + return this._gdpr_call_func(hasOptedOut, options); +}; + +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance + * + * ### Usage: + * + * // clear user's opt-in/out status + * mixpanel.clear_opt_in_out_tracking(); + * + * // clear user's opt-in/out status with specific cookie configuration - should match + * // configuration used when opt_in_tracking/opt_out_tracking methods were called. + * mixpanel.clear_opt_in_out_tracking({ + * cookie_expiration: 30, + * secure_cookie: true + * }); + * + * @param {Object} [options] A dictionary of config options to override + * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence + * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable + * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) + * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) + * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) + */ +MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { + options = _.extend({ + 'enable_persistence': true + }, options); + + this._gdpr_call_func(clearOptInOut, options); + this._gdpr_update_persistence(options); +}; + +MixpanelLib.prototype.report_error = function(msg, err) { + console.error.apply(console.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + console.error(err); + } +}; + +// EXPORTS (for closure compiler) + +// MixpanelLib Exports +MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; +MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; +MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; +MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; +MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; +MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; +MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; +MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; +MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; +MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; +MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; +MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; +MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; +MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; +MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; +MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; +MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; +MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; +MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; +MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; +MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; +MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; +MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; +MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; +MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; +MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; +MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; +MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; +MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; +MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; +MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; +MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; +MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; +MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; +MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; + +// MixpanelPersistence Exports +MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; +MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; +MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; +MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; +MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + +var instances = {}; +var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; +}; + +var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; +}; + +var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); +}; + +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; + init_type = INIT_MODULE; + mixpanel_master = new MixpanelLib(); + + override_mp_init_func(); + mixpanel_master['init'](); + add_dom_loaded_handler(); + + return mixpanel_master; +} + +// For loading separate bundles asynchronously via script tag + +// For builds that do NOT want any extra bundles (e.g. session recorder) +// and just the main SDK, throw an error when trying to load a separate bundle. +// eslint-disable-next-line no-unused-vars +function loadThrowError (src, _onload) { + throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); +} + +/* eslint camelcase: "off" */ + +var mixpanel = init_as_module(loadThrowError); + +module.exports = mixpanel; diff --git a/dist/mixpanel-recorder.js b/dist/mixpanel-recorder.js index f8f434fd..848cfd35 100644 --- a/dist/mixpanel-recorder.js +++ b/dist/mixpanel-recorder.js @@ -1432,7 +1432,7 @@ return doc.contains(n) || shadowHostInDom(n); } - var EventType = /* @__PURE__ */ ((EventType2) => { + var EventType$1 = /* @__PURE__ */ ((EventType2) => { EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; EventType2[EventType2["Load"] = 1] = "Load"; EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; @@ -1441,8 +1441,8 @@ EventType2[EventType2["Custom"] = 5] = "Custom"; EventType2[EventType2["Plugin"] = 6] = "Plugin"; return EventType2; - })(EventType || {}); - var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + })(EventType$1 || {}); + var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => { IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; @@ -1461,7 +1461,7 @@ IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; return IncrementalSource2; - })(IncrementalSource || {}); + })(IncrementalSource$1 || {}); var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; @@ -2180,10 +2180,10 @@ timeOffset: nowTimestamp() - timeBaseline, }); wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent - ? IncrementalSource.Drag + ? IncrementalSource$1.Drag : evt instanceof MouseEvent - ? IncrementalSource.MouseMove - : IncrementalSource.TouchMove); + ? IncrementalSource$1.MouseMove + : IncrementalSource$1.TouchMove); }), threshold, { trailing: false, })); @@ -3096,7 +3096,7 @@ transformCrossOriginEvent(iframeEl, e) { var _a; switch (e.type) { - case EventType.FullSnapshot: { + case EventType$1.FullSnapshot: { this.crossOriginIframeMirror.reset(iframeEl); this.crossOriginIframeStyleMirror.reset(iframeEl); this.replaceIdOnNode(e.data.node, iframeEl); @@ -3105,9 +3105,9 @@ this.patchRootIdOnNode(e.data.node, rootId); return { timestamp: e.timestamp, - type: EventType.IncrementalSnapshot, + type: EventType$1.IncrementalSnapshot, data: { - source: IncrementalSource.Mutation, + source: IncrementalSource$1.Mutation, adds: [ { parentId: this.mirror.getId(iframeEl), @@ -3122,21 +3122,21 @@ }, }; } - case EventType.Meta: - case EventType.Load: - case EventType.DomContentLoaded: { + case EventType$1.Meta: + case EventType$1.Load: + case EventType$1.DomContentLoaded: { return false; } - case EventType.Plugin: { + case EventType$1.Plugin: { return e; } - case EventType.Custom: { + case EventType$1.Custom: { this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); return e; } - case EventType.IncrementalSnapshot: { + case EventType$1.IncrementalSnapshot: { switch (e.data.source) { - case IncrementalSource.Mutation: { + case IncrementalSource$1.Mutation: { e.data.adds.forEach((n) => { this.replaceIds(n, iframeEl, [ 'parentId', @@ -3158,41 +3158,41 @@ }); return e; } - case IncrementalSource.Drag: - case IncrementalSource.TouchMove: - case IncrementalSource.MouseMove: { + case IncrementalSource$1.Drag: + case IncrementalSource$1.TouchMove: + case IncrementalSource$1.MouseMove: { e.data.positions.forEach((p) => { this.replaceIds(p, iframeEl, ['id']); }); return e; } - case IncrementalSource.ViewportResize: { + case IncrementalSource$1.ViewportResize: { return false; } - case IncrementalSource.MediaInteraction: - case IncrementalSource.MouseInteraction: - case IncrementalSource.Scroll: - case IncrementalSource.CanvasMutation: - case IncrementalSource.Input: { + case IncrementalSource$1.MediaInteraction: + case IncrementalSource$1.MouseInteraction: + case IncrementalSource$1.Scroll: + case IncrementalSource$1.CanvasMutation: + case IncrementalSource$1.Input: { this.replaceIds(e.data, iframeEl, ['id']); return e; } - case IncrementalSource.StyleSheetRule: - case IncrementalSource.StyleDeclaration: { + case IncrementalSource$1.StyleSheetRule: + case IncrementalSource$1.StyleDeclaration: { this.replaceIds(e.data, iframeEl, ['id']); this.replaceStyleIds(e.data, iframeEl, ['styleId']); return e; } - case IncrementalSource.Font: { + case IncrementalSource$1.Font: { return e; } - case IncrementalSource.Selection: { + case IncrementalSource$1.Selection: { e.data.ranges.forEach((range) => { this.replaceIds(range, iframeEl, ['start', 'end']); }); return e; } - case IncrementalSource.AdoptedStyleSheet: { + case IncrementalSource$1.AdoptedStyleSheet: { this.replaceIds(e.data, iframeEl, ['id']); this.replaceStyleIds(e.data, iframeEl, ['styleIds']); (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { @@ -4143,9 +4143,9 @@ wrappedEmit = (e, isCheckout) => { var _a; if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && - e.type !== EventType.FullSnapshot && - !(e.type === EventType.IncrementalSnapshot && - e.data.source === IncrementalSource.Mutation)) { + e.type !== EventType$1.FullSnapshot && + !(e.type === EventType$1.IncrementalSnapshot && + e.data.source === IncrementalSource$1.Mutation)) { mutationBuffers.forEach((buf) => buf.unfreeze()); } if (inEmittingFrame) { @@ -4160,12 +4160,12 @@ }; window.parent.postMessage(message, '*'); } - if (e.type === EventType.FullSnapshot) { + if (e.type === EventType$1.FullSnapshot) { lastFullSnapshotEvent = e; incrementalSnapshotCount = 0; } - else if (e.type === EventType.IncrementalSnapshot) { - if (e.data.source === IncrementalSource.Mutation && + else if (e.type === EventType$1.IncrementalSnapshot) { + if (e.data.source === IncrementalSource$1.Mutation && e.data.isAttachIframe) { return; } @@ -4180,21 +4180,21 @@ }; const wrappedMutationEmit = (m) => { wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.Mutation }, m), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Mutation }, m), })); }; const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.Scroll }, p), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Scroll }, p), })); const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.CanvasMutation }, p), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p), })); const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.AdoptedStyleSheet }, a), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a), })); const stylesheetManager = new StylesheetManager({ mutationCb: wrappedMutationEmit, @@ -4256,7 +4256,7 @@ return; } wrappedEmit(wrapEvent({ - type: EventType.Meta, + type: EventType$1.Meta, data: { href: window.location.href, width: getWindowWidth(), @@ -4303,7 +4303,7 @@ return console.warn('Failed to snapshot the document'); } wrappedEmit(wrapEvent({ - type: EventType.FullSnapshot, + type: EventType$1.FullSnapshot, data: { node, initialOffset: getWindowScroll(window), @@ -4320,52 +4320,52 @@ return callbackWrapper(initObservers)({ mutationCb: wrappedMutationEmit, mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, + type: EventType$1.IncrementalSnapshot, data: { source, positions, }, })), mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.MouseInteraction }, d), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d), })), scrollCb: wrappedScrollEmit, viewportResizeCb: (d) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.ViewportResize }, d), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d), })), inputCb: (v) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.Input }, v), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Input }, v), })), mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.MediaInteraction }, p), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p), })), styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.StyleSheetRule }, r), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r), })), styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.StyleDeclaration }, r), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r), })), canvasMutationCb: wrappedCanvasMutationEmit, fontCb: (p) => wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.Font }, p), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Font }, p), })), selectionCb: (p) => { wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.Selection }, p), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Selection }, p), })); }, customElementCb: (c) => { wrappedEmit(wrapEvent({ - type: EventType.IncrementalSnapshot, - data: Object.assign({ source: IncrementalSource.CustomElement }, c), + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CustomElement }, c), })); }, blockClass, @@ -4399,7 +4399,7 @@ observer: p.observer, options: p.options, callback: (payload) => wrappedEmit(wrapEvent({ - type: EventType.Plugin, + type: EventType$1.Plugin, data: { plugin: p.name, payload, @@ -4428,7 +4428,7 @@ else { handlers.push(on('DOMContentLoaded', () => { wrappedEmit(wrapEvent({ - type: EventType.DomContentLoaded, + type: EventType$1.DomContentLoaded, data: {}, })); if (recordAfter === 'DOMContentLoaded') @@ -4436,7 +4436,7 @@ })); handlers.push(on('load', () => { wrappedEmit(wrapEvent({ - type: EventType.Load, + type: EventType$1.Load, data: {}, })); if (recordAfter === 'load') @@ -4459,7 +4459,7 @@ throw new Error('please add custom event after start recording'); } wrappedEmit(wrapEvent({ - type: EventType.Custom, + type: EventType$1.Custom, data: { tag, payload, @@ -4477,9 +4477,40 @@ }; record.mirror = mirror; + var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType || {}); + var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource || {}); + var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -6344,8 +6375,779 @@ }; } + var logger$3 = console_with_prefix('lock'); + + /** + * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser + * window/tab at a time will be able to access shared resources. + * + * Based on the Alur and Taubenfeld fast lock + * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) + * with an added timeout to ensure there will be eventual progress in the event + * that a window is closed in the middle of the callback. + * + * Implementation based on the original version by David Wolever (https://github.com/wolever) + * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. + * + * @example + * const myLock = new SharedLock('some-key'); + * myLock.withLock(function() { + * console.log('I hold the mutex!'); + * }); + * + * @constructor + */ + var SharedLock = function(key, options) { + options = options || {}; + + this.storageKey = key; + this.storage = options.storage || window.localStorage; + this.pollIntervalMS = options.pollIntervalMS || 100; + this.timeoutMS = options.timeoutMS || 2000; + }; + + // pass in a specific pid to test contention scenarios; otherwise + // it is chosen randomly for each acquisition attempt + SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { + if (!pid && typeof errorCB !== 'function') { + pid = errorCB; + errorCB = null; + } + + var i = pid || (new Date().getTime() + '|' + Math.random()); + var startTime = new Date().getTime(); + + var key = this.storageKey; + var pollIntervalMS = this.pollIntervalMS; + var timeoutMS = this.timeoutMS; + var storage = this.storage; + + var keyX = key + ':X'; + var keyY = key + ':Y'; + var keyZ = key + ':Z'; + + var reportError = function(err) { + errorCB && errorCB(err); + }; + + var delay = function(cb) { + if (new Date().getTime() - startTime > timeoutMS) { + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + storage.removeItem(keyZ); + storage.removeItem(keyY); + loop(); + return; + } + setTimeout(function() { + try { + cb(); + } catch(err) { + reportError(err); + } + }, pollIntervalMS * (Math.random() + 0.1)); + }; + + var waitFor = function(predicate, cb) { + if (predicate()) { + cb(); + } else { + delay(function() { + waitFor(predicate, cb); + }); + } + }; + + var getSetY = function() { + var valY = storage.getItem(keyY); + if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) + return false; + } else { + storage.setItem(keyY, i); + if (storage.getItem(keyY) === i) { + return true; + } else { + if (!localStorageSupported(storage, true)) { + throw new Error('localStorage support dropped while acquiring lock'); + } + return false; + } + } + }; + + var loop = function() { + storage.setItem(keyX, i); + + waitFor(getSetY, function() { + if (storage.getItem(keyX) === i) { + criticalSection(); + return; + } + + delay(function() { + if (storage.getItem(keyY) !== i) { + loop(); + return; + } + waitFor(function() { + return !storage.getItem(keyZ); + }, criticalSection); + }); + }); + }; + + var criticalSection = function() { + storage.setItem(keyZ, '1'); + try { + lockedCB(); + } finally { + storage.removeItem(keyZ); + if (storage.getItem(keyY) === i) { + storage.removeItem(keyY); + } + if (storage.getItem(keyX) === i) { + storage.removeItem(keyX); + } + } + }; + + try { + if (localStorageSupported(storage, true)) { + loop(); + } else { + throw new Error('localStorage support check failed'); + } + } catch(err) { + reportError(err); + } + }; + + var logger$2 = console_with_prefix('batch'); + + /** + * RequestQueue: queue for batching API requests with localStorage backup for retries. + * Maintains an in-memory queue which represents the source of truth for the current + * page, but also writes all items out to a copy in the browser's localStorage, which + * can be read on subsequent pageloads and retried. For batchability, all the request + * items in the queue should be of the same type (events, people updates, group updates) + * so they can be sent in a single request to the same API endpoint. + * + * LocalStorage keying and locking: In order for reloads and subsequent pageloads of + * the same site to access the same persisted data, they must share the same localStorage + * key (for instance based on project token and queue type). Therefore access to the + * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent + * simultaneously open windows/tabs from overwriting each other's data (which would lead + * to data loss in some situations). + * @constructor + */ + var RequestQueue = function(storageKey, options) { + options = options || {}; + this.storageKey = storageKey; + this.storage = options.storage || window.localStorage; + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); + this.lock = new SharedLock(storageKey, {storage: this.storage}); + + this.usePersistence = options.usePersistence; + this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios + + this.memQueue = []; + }; + + /** + * Add one item to queues (memory and localStorage). The queued entry includes + * the given item along with an auto-generated ID and a "flush-after" timestamp. + * It is expected that the item will be sent over the network and dequeued + * before the flush-after time; if this doesn't happen it is considered orphaned + * (e.g., the original tab where it was enqueued got closed before it could be + * sent) and the item can be sent by any tab that finds it in localStorage. + * + * The final callback param is called with a param indicating success or + * failure of the enqueue operation; it is asynchronous because the localStorage + * lock is asynchronous. + */ + RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { + var queueEntry = { + 'id': cheap_guid(), + 'flushAfter': new Date().getTime() + flushInterval * 2, + 'payload': item + }; + + if (!this.usePersistence) { + this.memQueue.push(queueEntry); + if (cb) { + cb(true); + } + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; + + /** + * Read out the given number of queue entries. If this.memQueue + * has fewer than batchSize items, then look for "orphaned" items + * in the persisted queue (items where the 'flushAfter' time has + * already passed). + */ + RequestQueue.prototype.fillBatch = function(batchSize) { + var batch = this.memQueue.slice(0, batchSize); + if (this.usePersistence && batch.length < batchSize) { + // don't need lock just to read events; localStorage is thread-safe + // and the worst that could happen is a duplicate send of some + // orphaned events, which will be deduplicated on the server side + var storedQueue = this.readFromStorage(); + if (storedQueue.length) { + // item IDs already in batch; don't duplicate out of storage + var idsInBatch = {}; // poor man's Set + _.each(batch, function(item) { idsInBatch[item['id']] = true; }); + + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { + item.orphaned = true; + batch.push(item); + if (batch.length >= batchSize) { + break; + } + } + } + } + } + return batch; + }; + + /** + * Remove items with matching 'id' from array (immutably) + * also remove any item without a valid id (e.g., malformed + * storage entries). + */ + var filterOutIDsAndInvalid = function(items, idSet) { + var filteredItems = []; + _.each(items, function(item) { + if (item['id'] && !idSet[item['id']]) { + filteredItems.push(item); + } + }); + return filteredItems; + }; + + /** + * Remove items with matching IDs from both in-memory queue + * and persisted queue + */ + RequestQueue.prototype.removeItemsByID = function(ids, cb) { + var idSet = {}; // poor man's Set + _.each(ids, function(id) { idSet[id] = true; }); + + this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } + } + } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; + } + return succeeded; + }, this); + + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } + } + } + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + + }; + + // internal helper for RequestQueue.updatePayloads + var updatePayloads = function(existingItems, itemsToUpdate) { + var newItems = []; + _.each(existingItems, function(item) { + var id = item['id']; + if (id in itemsToUpdate) { + var newPayload = itemsToUpdate[id]; + if (newPayload !== null) { + item['payload'] = newPayload; + newItems.push(item); + } + } else { + // no update + newItems.push(item); + } + }); + return newItems; + }; + + /** + * Update payloads of given items in both in-memory queue and + * persisted queue. Items set to null are removed from queues. + */ + RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { + this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + + }; + + /** + * Read and parse items array from localStorage entry, handling + * malformed/missing data if necessary. + */ + RequestQueue.prototype.readFromStorage = function() { + var storageEntry; + try { + storageEntry = this.storage.getItem(this.storageKey); + if (storageEntry) { + storageEntry = JSONParse(storageEntry); + if (!_.isArray(storageEntry)) { + this.reportError('Invalid storage entry:', storageEntry); + storageEntry = null; + } + } + } catch (err) { + this.reportError('Error retrieving queue', err); + storageEntry = null; + } + return storageEntry || []; + }; + + /** + * Serialize the given items array to localStorage. + */ + RequestQueue.prototype.saveToStorage = function(queue) { + try { + this.storage.setItem(this.storageKey, JSONStringify(queue)); + return true; + } catch (err) { + this.reportError('Error saving queue', err); + return false; + } + }; + + /** + * Clear out queues (memory and localStorage). + */ + RequestQueue.prototype.clear = function() { + this.memQueue = []; + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } + }; + + // maximum interval between request retries after exponential backoff + var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + + var logger$1 = console_with_prefix('batch'); + + /** + * RequestBatcher: manages the queueing, flushing, retry etc of requests of one + * type (events, people, groups). + * Uses RequestQueue to manage the backing store. + * @constructor + */ + var RequestBatcher = function(storageKey, options) { + this.errorReporter = options.errorReporter; + this.queue = new RequestQueue(storageKey, { + errorReporter: _.bind(this.reportError, this), + storage: options.storage, + usePersistence: options.usePersistence + }); + + this.libConfig = options.libConfig; + this.sendRequest = options.sendRequestFunc; + this.beforeSendHook = options.beforeSendHook; + this.stopAllBatching = options.stopAllBatchingFunc; + + // seed variable batch size + flush interval with configured values + this.batchSize = this.libConfig['batch_size']; + this.flushInterval = this.libConfig['batch_flush_interval_ms']; + + this.stopped = !this.libConfig['batch_autostart']; + this.consecutiveRemovalFailures = 0; + + // extra client-side dedupe + this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; + }; + + /** + * Add one item to queue. + */ + RequestBatcher.prototype.enqueue = function(item, cb) { + this.queue.enqueue(item, this.flushInterval, cb); + }; + + /** + * Start flushing batches at the configured time interval. Must call + * this method upon SDK init in order to send anything over the network. + */ + RequestBatcher.prototype.start = function() { + this.stopped = false; + this.consecutiveRemovalFailures = 0; + this.flush(); + }; + + /** + * Stop flushing batches. Can be restarted by calling start(). + */ + RequestBatcher.prototype.stop = function() { + this.stopped = true; + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } + }; + + /** + * Clear out queue. + */ + RequestBatcher.prototype.clear = function() { + this.queue.clear(); + }; + + /** + * Restore batch size configuration to whatever is set in the main SDK. + */ + RequestBatcher.prototype.resetBatchSize = function() { + this.batchSize = this.libConfig['batch_size']; + }; + + /** + * Restore flush interval time configuration to whatever is set in the main SDK. + */ + RequestBatcher.prototype.resetFlush = function() { + this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); + }; + + /** + * Schedule the next flush in the given number of milliseconds. + */ + RequestBatcher.prototype.scheduleFlush = function(flushMS) { + this.flushInterval = flushMS; + if (!this.stopped) { // don't schedule anymore if batching has been stopped + this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); + } + }; + + /** + * Flush one batch to network. Depending on success/failure modes, it will either + * remove the batch from the queue or leave it in for retry, and schedule the next + * flush. In cases of most network or API failures, it will back off exponentially + * when retrying. + * @param {Object} [options] + * @param {boolean} [options.sendBeacon] - whether to send batch with + * navigator.sendBeacon (only useful for sending batches before page unloads, as + * sendBeacon offers no callbacks or status indications) + */ + RequestBatcher.prototype.flush = function(options) { + try { + + if (this.requestInProgress) { + logger$1.log('Flush: Request already in progress'); + return; + } + + options = options || {}; + var timeoutMS = this.libConfig['batch_request_timeout_ms']; + var startTime = new Date().getTime(); + var currentBatchSize = this.batchSize; + var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; + var dataForRequest = []; + var transformedItems = {}; + _.each(batch, function(item) { + var payload = item['payload']; + if (this.beforeSendHook && !item.orphaned) { + payload = this.beforeSendHook(payload); + } + if (payload) { + // mp_sent_by_lib_version prop captures which lib version actually + // sends each event (regardless of which version originally queued + // it for sending) + if (payload['event'] && payload['properties']) { + payload['properties'] = _.extend( + {}, + payload['properties'], + {'mp_sent_by_lib_version': Config.LIB_VERSION} + ); + } + var addPayload = true; + var itemId = item['id']; + if (itemId) { + if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { + this.reportError('[dupe] item ID sent too many times, not sending', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + addPayload = false; + } + } else { + this.reportError('[dupe] found item with no ID', {item: item}); + } + + if (addPayload) { + dataForRequest.push(payload); + } + } + transformedItems[item['id']] = payload; + }, this); + if (dataForRequest.length < 1) { + this.resetFlush(); + return; // nothing to do + } + + this.requestInProgress = true; + + var batchSendCallback = _.bind(function(res) { + this.requestInProgress = false; + + try { + + // handle API response in a try-catch to make sure we can reset the + // flush operation if something goes wrong + + var removeItemsFromQueue = false; + if (options.unloading) { + // update persisted data to include hook transformations + this.queue.updatePayloads(transformedItems); + } else if ( + _.isObject(res) && + res.error === 'timeout' && + new Date().getTime() - startTime >= timeoutMS + ) { + this.reportError('Network timeout; retrying'); + this.flush(); + } else if ( + _.isObject(res) && + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') + ) { + // network or API error, or 429 Too Many Requests, retry + var retryMS = this.flushInterval * 2; + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; + } + retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); + this.reportError('Error; retry in ' + retryMS + ' ms'); + this.scheduleFlush(retryMS); + } else if (_.isObject(res) && res.httpStatusCode === 413) { + // 413 Payload Too Large + if (batch.length > 1) { + var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); + this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); + this.reportError('413 response; reducing batch size to ' + this.batchSize); + this.resetFlush(); + } else { + this.reportError('Single-event request too large; dropping', batch); + this.resetBatchSize(); + removeItemsFromQueue = true; + } + } else { + // successful network request+response; remove each item in batch from queue + // (even if it was e.g. a 400, in which case retrying won't help) + removeItemsFromQueue = true; + } + + if (removeItemsFromQueue) { + this.queue.removeItemsByID( + _.map(batch, function(item) { return item['id']; }), + _.bind(function(succeeded) { + if (succeeded) { + this.consecutiveRemovalFailures = 0; + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } + } else { + this.reportError('Failed to remove items from queue'); + if (++this.consecutiveRemovalFailures > 5) { + this.reportError('Too many queue failures; disabling batching system.'); + this.stopAllBatching(); + } else { + this.resetFlush(); + } + } + }, this) + ); + + // client-side dedupe + _.each(batch, _.bind(function(item) { + var itemId = item['id']; + if (itemId) { + this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; + this.itemIdsSentSuccessfully[itemId]++; + if (this.itemIdsSentSuccessfully[itemId] > 5) { + this.reportError('[dupe] item ID sent too many times', { + item: item, + batchSize: batch.length, + timesSent: this.itemIdsSentSuccessfully[itemId] + }); + } + } else { + this.reportError('[dupe] found item with no ID while removing', {item: item}); + } + }, this)); + } + + } catch(err) { + this.reportError('Error handling API response', err); + this.resetFlush(); + } + }, this); + var requestOptions = { + method: 'POST', + verbose: true, + ignore_json_errors: true, // eslint-disable-line camelcase + timeout_ms: timeoutMS // eslint-disable-line camelcase + }; + if (options.unloading) { + requestOptions.transport = 'sendBeacon'; + } + logger$1.log('MIXPANEL REQUEST:', dataForRequest); + this.sendRequest(dataForRequest, requestOptions, batchSendCallback); + } catch(err) { + this.reportError('Error flushing request queue', err); + this.resetFlush(); + } + }; + + /** + * Log error to global logger and optional user-defined logger. + */ + RequestBatcher.prototype.reportError = function(msg, err) { + logger$1.error.apply(logger$1.error, arguments); + if (this.errorReporter) { + try { + if (!(err instanceof Error)) { + err = new Error(msg); + } + this.errorReporter(msg, err); + } catch(err) { + logger$1.error(err); + } + } + }; + var logger = console_with_prefix('recorder'); - var CompressionStream = window['CompressionStream']; + var CompressionStream = win['CompressionStream']; + + var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true + }; + + var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, + ]); + + function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); + } var MixpanelRecorder = function(mixpanelInstance) { this._mixpanel = mixpanelInstance; @@ -6357,14 +7159,24 @@ this.seqNo = 0; this.replayId = null; this.replayStartTime = null; - this.batchStartTime = null; - this.replayLengthMs = 0; this.sendBatchId = null; this.idleTimeoutId = null; this.maxTimeoutId = null; this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); + }; + + + MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); }; // eslint-disable-next-line camelcase @@ -6386,12 +7198,11 @@ this.recEvents = []; this.seqNo = 0; - this.startDate = new Date(); - this.replayStartTime = this.startDate.getTime(); - this.batchStartTime = this.replayStartTime; + this.replayStartTime = null; this.replayId = _.UUID(); - this.replayLengthMs = 0; + + this.batcher.start(); var resetIdleTimeout = _.bind(function () { clearTimeout(this.idleTimeoutId); @@ -6403,20 +7214,22 @@ this._stopRecording = record({ 'emit': _.bind(function (ev) { - this.recEvents.push(ev); - this.replayLengthMs = new Date().getTime() - this.replayStartTime; - resetIdleTimeout(); + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } }, this), - 'maskAllInputs': true, - 'maskTextSelector': this.get_config('record_mask_text_selector'), + 'blockClass': this.get_config('record_block_class'), 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, 'maskTextClass': this.get_config('record_mask_text_class'), - 'blockClass': this.get_config('record_block_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); resetIdleTimeout(); - this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000); this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); }; @@ -6431,10 +7244,9 @@ this._stopRecording = null; } - this._flushEvents(); // flush any remaining events + this.batcher.flush(); // flush any remaining events this.replayId = null; - clearInterval(this.sendBatchId); clearTimeout(this.idleTimeoutId); clearTimeout(this.maxTimeoutId); }; @@ -6443,8 +7255,8 @@ * Flushes the current batch of events to the server, but passes an opt-out callback to make sure * we stop recording and dump any queued events if the user has opted out. */ - MixpanelRecorder.prototype.flushEventsWithOptOut = function () { - this._flushEvents(_.bind(this._onOptOut, this)); + MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); }; MixpanelRecorder.prototype._onOptOut = function (code) { @@ -6455,33 +7267,60 @@ } }; - MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) { - window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { 'method': 'POST', 'headers': { 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), 'Content-Type': 'application/octet-stream' }, - 'body': reqBody + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); }; - /** - * @api private - * Private method, flushes the current batch of events to the server. - */ - MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() { - var numEvents = this.recEvents.length; + MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + var reqParams = { 'distinct_id': String(this._mixpanel.get_distinct_id()), - 'seq': this.seqNo++, - 'batch_start_time': this.batchStartTime / 1000, + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, 'replay_id': this.replayId, - 'replay_length_ms': this.replayLengthMs, + 'replay_length_ms': replayLengthMs, 'replay_start_time': this.replayStartTime / 1000 }; - var eventsJson = _.JSONEncode(this.recEvents); + var eventsJson = _.JSONEncode(data); // send ID management props if they exist var deviceId = this._mixpanel.get_property('$device_id'); @@ -6493,8 +7332,6 @@ reqParams['$user_id'] = userId; } - this.recEvents = this.recEvents.slice(numEvents); - this.batchStartTime = new Date().getTime(); if (CompressionStream) { var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); @@ -6502,15 +7339,29 @@ .blob() .then(_.bind(function(compressedBlob) { reqParams['format'] = 'gzip'; - this._sendRequest(reqParams, compressedBlob); + this._sendRequest(reqParams, compressedBlob, callback); }, this)); } else { reqParams['format'] = 'body'; - this._sendRequest(reqParams, eventsJson); + this._sendRequest(reqParams, eventsJson, callback); } } }); - window['__mp_recorder'] = MixpanelRecorder; + + MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } + }; + + + win['__mp_recorder'] = MixpanelRecorder; })(); diff --git a/dist/mixpanel-recorder.min.js b/dist/mixpanel-recorder.min.js index 9cb0d896..593e5f5a 100644 --- a/dist/mixpanel-recorder.min.js +++ b/dist/mixpanel-recorder.min.js @@ -1,6 +1,6 @@ -(function(){"use strict";var A;(function(e){e[e.Document=0]="Document",e[e.DocumentType=1]="DocumentType",e[e.Element=2]="Element",e[e.Text=3]="Text",e[e.CDATA=4]="CDATA",e[e.Comment=5]="Comment"})(A||(A={}));function ar(e){return e.nodeType===e.ELEMENT_NODE}function ge(e){const t=e?.host;return t?.shadowRoot===e}function ye(e){return Object.prototype.toString.call(e)==="[object ShadowRoot]"}function lr(e){return e.includes(" background-clip: text;")&&!e.includes(" -webkit-background-clip: text;")&&(e=e.replace(" background-clip: text;"," -webkit-background-clip: text; background-clip: text;")),e}function cr(e){const{cssText:t}=e;if(t.split('"').length<3)return t;const r=["@import",`url(${JSON.stringify(e.href)})`];return e.layerName===""?r.push("layer"):e.layerName&&r.push(`layer(${e.layerName})`),e.supportsText&&r.push(`supports(${e.supportsText})`),e.media.length&&r.push(e.media.mediaText),r.join(" ")+";"}function _e(e){try{const t=e.rules||e.cssRules;return t?lr(Array.from(t,ft).join("")):null}catch{return null}}function ft(e){let t;if(dr(e))try{t=_e(e.styleSheet)||cr(e)}catch{}else if(fr(e)&&e.selectorText.includes(":"))return ur(e.cssText);return t||e.cssText}function ur(e){const t=/(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;return e.replace(t,"$1\\$2")}function dr(e){return"styleSheet"in e}function fr(e){return"selectorText"in e}class ht{constructor(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}getId(t){var r;if(!t)return-1;const n=(r=this.getMeta(t))===null||r===void 0?void 0:r.id;return n??-1}getNode(t){return this.idNodeMap.get(t)||null}getIds(){return Array.from(this.idNodeMap.keys())}getMeta(t){return this.nodeMetaMap.get(t)||null}removeNodeFromMap(t){const r=this.getId(t);this.idNodeMap.delete(r),t.childNodes&&t.childNodes.forEach(n=>this.removeNodeFromMap(n))}has(t){return this.idNodeMap.has(t)}hasNode(t){return this.nodeMetaMap.has(t)}add(t,r){const n=r.id;this.idNodeMap.set(n,t),this.nodeMetaMap.set(t,r)}replace(t,r){const n=this.getNode(t);if(n){const i=this.nodeMetaMap.get(n);i&&this.nodeMetaMap.set(r,i)}this.idNodeMap.set(t,r)}reset(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}}function hr(){return new ht}function Ve({element:e,maskInputOptions:t,tagName:r,type:n,value:i,maskInputFn:o}){let a=i||"";const l=n&&te(n);return(t[r.toLowerCase()]||l&&t[l])&&(o?a=o(a,e):a="*".repeat(a.length)),a}function te(e){return e.toLowerCase()}const pt="__rrweb_original__";function pr(e){const t=e.getContext("2d");if(!t)return!0;const r=50;for(let n=0;ns!==0))return!1}return!0}function qe(e){const t=e.type;return e.hasAttribute("data-rr-is-password")?"password":t?te(t):null}function mt(e,t){var r;let n;try{n=new URL(e,t??window.location.href)}catch{return null}const i=/\.([0-9a-z]+)(?:$)/i,o=n.pathname.match(i);return(r=o?.[1])!==null&&r!==void 0?r:null}let mr=1;const gr=new RegExp("[^a-z0-9-_:]"),Se=-2;function gt(){return mr++}function yr(e){if(e instanceof HTMLFormElement)return"form";const t=te(e.tagName);return gr.test(t)?"div":t}function Sr(e){let t="";return e.indexOf("//")>-1?t=e.split("/").slice(0,3).join("/"):t=e.split("/")[0],t=t.split("?")[0],t}let ce,yt;const vr=/url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm,br=/^(?:[a-z+]+:)?\/\//i,wr=/^www\..*/i,Mr=/^(data:)([^,]*),(.*)/i;function xe(e,t){return(e||"").replace(vr,(r,n,i,o,a,l)=>{const s=i||a||l,c=n||o||"";if(!s)return r;if(br.test(s)||wr.test(s))return`url(${c}${s}${c})`;if(Mr.test(s))return`url(${c}${s}${c})`;if(s[0]==="/")return`url(${c}${Sr(t)+s}${c})`;const u=t.split("/"),p=s.split("/");u.pop();for(const m of p)m!=="."&&(m===".."?u.pop():u.push(m));return`url(${c}${u.join("/")}${c})`})}const Ir=/^[^ \t\n\r\u000c]+/,Cr=/^[, \t\n\r\u000c]+/;function Or(e,t){if(t.trim()==="")return t;let r=0;function n(o){let a;const l=o.exec(t.substring(r));return l?(a=l[0],r+=a.length,a):""}const i=[];for(;n(Cr),!(r>=t.length);){let o=n(Ir);if(o.slice(-1)===",")o=ue(e,o.substring(0,o.length-1)),i.push(o);else{let a="";o=ue(e,o);let l=!1;for(;;){const s=t.charAt(r);if(s===""){i.push((o+a).trim());break}else if(l)s===")"&&(l=!1);else if(s===","){r+=1,i.push((o+a).trim());break}else s==="("&&(l=!0);a+=s,r+=1}}}return i.join(", ")}function ue(e,t){if(!t||t.trim()==="")return t;const r=e.createElement("a");return r.href=t,r.href}function _r(e){return!!(e.tagName==="svg"||e.ownerSVGElement)}function Je(){const e=document.createElement("a");return e.href="",e.href}function St(e,t,r,n){return n&&(r==="src"||r==="href"&&!(t==="use"&&n[0]==="#")||r==="xlink:href"&&n[0]!=="#"||r==="background"&&(t==="table"||t==="td"||t==="th")?ue(e,n):r==="srcset"?Or(e,n):r==="style"?xe(n,Je()):t==="object"&&r==="data"?ue(e,n):n)}function vt(e,t,r){return(e==="video"||e==="audio")&&t==="autoplay"}function xr(e,t,r){try{if(typeof t=="string"){if(e.classList.contains(t))return!0}else for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}if(r)return e.matches(r)}catch{}return!1}function Ee(e,t,r){if(!e)return!1;if(e.nodeType!==e.ELEMENT_NODE)return r?Ee(e.parentNode,t,r):!1;for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}return r?Ee(e.parentNode,t,r):!1}function bt(e,t,r,n){try{const i=e.nodeType===e.ELEMENT_NODE?e:e.parentElement;if(i===null)return!1;if(typeof t=="string"){if(n){if(i.closest(`.${t}`))return!0}else if(i.classList.contains(t))return!0}else if(Ee(i,t,n))return!0;if(r){if(n){if(i.closest(r))return!0}else if(i.matches(r))return!0}}catch{}return!1}function Er(e,t,r){const n=e.contentWindow;if(!n)return;let i=!1,o;try{o=n.document.readyState}catch{return}if(o!=="complete"){const l=setTimeout(()=>{i||(t(),i=!0)},r);e.addEventListener("load",()=>{clearTimeout(l),i=!0,t()});return}const a="about:blank";if(n.location.href!==a||e.src===a||e.src==="")return setTimeout(t,0),e.addEventListener("load",t);e.addEventListener("load",t)}function kr(e,t,r){let n=!1,i;try{i=e.sheet}catch{return}if(i)return;const o=setTimeout(()=>{n||(t(),n=!0)},r);e.addEventListener("load",()=>{clearTimeout(o),n=!0,t()})}function Tr(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:a,inlineStylesheet:l,maskInputOptions:s={},maskTextFn:c,maskInputFn:u,dataURLOptions:p={},inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:h=!1}=t,y=Rr(r,n);switch(e.nodeType){case e.DOCUMENT_NODE:return e.compatMode!=="CSS1Compat"?{type:A.Document,childNodes:[],compatMode:e.compatMode}:{type:A.Document,childNodes:[]};case e.DOCUMENT_TYPE_NODE:return{type:A.DocumentType,name:e.name,publicId:e.publicId,systemId:e.systemId,rootId:y};case e.ELEMENT_NODE:return Dr(e,{doc:r,blockClass:i,blockSelector:o,inlineStylesheet:l,maskInputOptions:s,maskInputFn:u,dataURLOptions:p,inlineImages:m,recordCanvas:f,keepIframeSrcFn:g,newlyAddedElement:h,rootId:y});case e.TEXT_NODE:return Nr(e,{needsMask:a,maskTextFn:c,rootId:y});case e.CDATA_SECTION_NODE:return{type:A.CDATA,textContent:"",rootId:y};case e.COMMENT_NODE:return{type:A.Comment,textContent:e.textContent||"",rootId:y};default:return!1}}function Rr(e,t){if(!t.hasNode(e))return;const r=t.getId(e);return r===1?void 0:r}function Nr(e,t){var r;const{needsMask:n,maskTextFn:i,rootId:o}=t,a=e.parentNode&&e.parentNode.tagName;let l=e.textContent;const s=a==="STYLE"?!0:void 0,c=a==="SCRIPT"?!0:void 0;if(s&&l){try{e.nextSibling||e.previousSibling||!((r=e.parentNode.sheet)===null||r===void 0)&&r.cssRules&&(l=_e(e.parentNode.sheet))}catch(u){console.warn(`Cannot get CSS styles from text's parentNode. Error: ${u}`,e)}l=xe(l,Je())}return c&&(l="SCRIPT_PLACEHOLDER"),!s&&!c&&l&&n&&(l=i?i(l,e.parentElement):l.replace(/[\S]/g,"*")),{type:A.Text,textContent:l||"",isStyle:s,rootId:o}}function Dr(e,t){const{doc:r,blockClass:n,blockSelector:i,inlineStylesheet:o,maskInputOptions:a={},maskInputFn:l,dataURLOptions:s={},inlineImages:c,recordCanvas:u,keepIframeSrcFn:p,newlyAddedElement:m=!1,rootId:f}=t,g=xr(e,n,i),h=yr(e);let y={};const w=e.attributes.length;for(let S=0;SM.href===e.href);let b=null;S&&(b=_e(S)),b&&(delete y.rel,delete y.href,y._cssText=xe(b,S.href))}if(h==="style"&&e.sheet&&!(e.innerText||e.textContent||"").trim().length){const S=_e(e.sheet);S&&(y._cssText=xe(S,Je()))}if(h==="input"||h==="textarea"||h==="select"){const S=e.value,b=e.checked;y.type!=="radio"&&y.type!=="checkbox"&&y.type!=="submit"&&y.type!=="button"&&S?y.value=Ve({element:e,type:qe(e),tagName:h,value:S,maskInputOptions:a,maskInputFn:l}):b&&(y.checked=b)}if(h==="option"&&(e.selected&&!a.select?y.selected=!0:delete y.selected),h==="canvas"&&u){if(e.__context==="2d")pr(e)||(y.rr_dataURL=e.toDataURL(s.type,s.quality));else if(!("__context"in e)){const S=e.toDataURL(s.type,s.quality),b=document.createElement("canvas");b.width=e.width,b.height=e.height;const M=b.toDataURL(s.type,s.quality);S!==M&&(y.rr_dataURL=S)}}if(h==="img"&&c){ce||(ce=r.createElement("canvas"),yt=ce.getContext("2d"));const S=e,b=S.crossOrigin;S.crossOrigin="anonymous";const M=()=>{S.removeEventListener("load",M);try{ce.width=S.naturalWidth,ce.height=S.naturalHeight,yt.drawImage(S,0,0),y.rr_dataURL=ce.toDataURL(s.type,s.quality)}catch(F){console.warn(`Cannot inline img src=${S.currentSrc}! Error: ${F}`)}b?y.crossOrigin=b:S.removeAttribute("crossorigin")};S.complete&&S.naturalWidth!==0?M():S.addEventListener("load",M)}if(h==="audio"||h==="video"){const S=y;S.rr_mediaState=e.paused?"paused":"played",S.rr_mediaCurrentTime=e.currentTime,S.rr_mediaPlaybackRate=e.playbackRate,S.rr_mediaMuted=e.muted,S.rr_mediaLoop=e.loop,S.rr_mediaVolume=e.volume}if(m||(e.scrollLeft&&(y.rr_scrollLeft=e.scrollLeft),e.scrollTop&&(y.rr_scrollTop=e.scrollTop)),g){const{width:S,height:b}=e.getBoundingClientRect();y={class:y.class,rr_width:`${S}px`,rr_height:`${b}px`}}h==="iframe"&&!p(y.src)&&(e.contentDocument||(y.rr_src=y.src),delete y.src);let v;try{customElements.get(h)&&(v=!0)}catch{}return{type:A.Element,tagName:h,attributes:y,childNodes:[],isSVG:_r(e)||void 0,needBlock:g,rootId:f,isCustom:v}}function E(e){return e==null?"":e.toLowerCase()}function Ar(e,t){if(t.comment&&e.type===A.Comment)return!0;if(e.type===A.Element){if(t.script&&(e.tagName==="script"||e.tagName==="link"&&(e.attributes.rel==="preload"||e.attributes.rel==="modulepreload")&&e.attributes.as==="script"||e.tagName==="link"&&e.attributes.rel==="prefetch"&&typeof e.attributes.href=="string"&&mt(e.attributes.href)==="js"))return!0;if(t.headFavicon&&(e.tagName==="link"&&e.attributes.rel==="shortcut icon"||e.tagName==="meta"&&(E(e.attributes.name).match(/^msapplication-tile(image|color)$/)||E(e.attributes.name)==="application-name"||E(e.attributes.rel)==="icon"||E(e.attributes.rel)==="apple-touch-icon"||E(e.attributes.rel)==="shortcut icon")))return!0;if(e.tagName==="meta"){if(t.headMetaDescKeywords&&E(e.attributes.name).match(/^description|keywords$/))return!0;if(t.headMetaSocial&&(E(e.attributes.property).match(/^(og|twitter|fb):/)||E(e.attributes.name).match(/^(og|twitter):/)||E(e.attributes.name)==="pinterest"))return!0;if(t.headMetaRobots&&(E(e.attributes.name)==="robots"||E(e.attributes.name)==="googlebot"||E(e.attributes.name)==="bingbot"))return!0;if(t.headMetaHttpEquiv&&e.attributes["http-equiv"]!==void 0)return!0;if(t.headMetaAuthorship&&(E(e.attributes.name)==="author"||E(e.attributes.name)==="generator"||E(e.attributes.name)==="framework"||E(e.attributes.name)==="publisher"||E(e.attributes.name)==="progid"||E(e.attributes.property).match(/^article:/)||E(e.attributes.property).match(/^product:/)))return!0;if(t.headMetaVerification&&(E(e.attributes.name)==="google-site-verification"||E(e.attributes.name)==="yandex-verification"||E(e.attributes.name)==="csrf-token"||E(e.attributes.name)==="p:domain_verify"||E(e.attributes.name)==="verify-v1"||E(e.attributes.name)==="verification"||E(e.attributes.name)==="shopify-checkout-api-token"))return!0}}return!1}function de(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,maskTextClass:a,maskTextSelector:l,skipChild:s=!1,inlineStylesheet:c=!0,maskInputOptions:u={},maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g={},inlineImages:h=!1,recordCanvas:y=!1,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S=5e3,onStylesheetLoad:b,stylesheetLoadTimeout:M=5e3,keepIframeSrcFn:F=()=>!1,newlyAddedElement:P=!1}=t;let{needsMask:k}=t,{preserveWhiteSpace:T=!0}=t;!k&&e.childNodes&&(k=bt(e,a,l,k===void 0));const j=Tr(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,dataURLOptions:g,inlineImages:h,recordCanvas:y,keepIframeSrcFn:F,newlyAddedElement:P});if(!j)return console.warn(e,"not serialized"),null;let V;n.hasNode(e)?V=n.getId(e):Ar(j,f)||!T&&j.type===A.Text&&!j.isStyle&&!j.textContent.replace(/^\s+|\s+$/gm,"").length?V=Se:V=gt();const x=Object.assign(j,{id:V});if(n.add(e,x),V===Se)return null;w&&w(e);let oe=!s;if(x.type===A.Element){oe=oe&&!x.needBlock,delete x.needBlock;const H=e.shadowRoot;H&&ye(H)&&(x.isShadowHost=!0)}if((x.type===A.Document||x.type===A.Element)&&oe){f.headWhitespace&&x.type===A.Element&&x.tagName==="head"&&(T=!1);const H={doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:s,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F};if(!(x.type===A.Element&&x.tagName==="textarea"&&x.attributes.value!==void 0))for(const ee of Array.from(e.childNodes)){const K=de(ee,H);K&&x.childNodes.push(K)}if(ar(e)&&e.shadowRoot)for(const ee of Array.from(e.shadowRoot.childNodes)){const K=de(ee,H);K&&(ye(e.shadowRoot)&&(K.isShadow=!0),x.childNodes.push(K))}}return e.parentNode&&ge(e.parentNode)&&ye(e.parentNode)&&(x.isShadow=!0),x.type===A.Element&&x.tagName==="iframe"&&Er(e,()=>{const H=e.contentDocument;if(H&&v){const ee=de(H,{doc:H,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F});ee&&v(e,ee)}},S),x.type===A.Element&&x.tagName==="link"&&typeof x.attributes.rel=="string"&&(x.attributes.rel==="stylesheet"||x.attributes.rel==="preload"&&typeof x.attributes.href=="string"&&mt(x.attributes.href)==="css")&&kr(e,()=>{if(b){const H=de(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:k,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f,dataURLOptions:g,inlineImages:h,recordCanvas:y,preserveWhiteSpace:T,onSerialize:w,onIframeLoad:v,iframeLoadTimeout:S,onStylesheetLoad:b,stylesheetLoadTimeout:M,keepIframeSrcFn:F});H&&b(e,H)}},M),x}function Lr(e,t){const{mirror:r=new ht,blockClass:n="rr-block",blockSelector:i=null,maskTextClass:o="rr-mask",maskTextSelector:a=null,inlineStylesheet:l=!0,inlineImages:s=!1,recordCanvas:c=!1,maskAllInputs:u=!1,maskTextFn:p,maskInputFn:m,slimDOM:f=!1,dataURLOptions:g,preserveWhiteSpace:h,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:v,onStylesheetLoad:S,stylesheetLoadTimeout:b,keepIframeSrcFn:M=()=>!1}=t||{};return de(e,{doc:e,mirror:r,blockClass:n,blockSelector:i,maskTextClass:o,maskTextSelector:a,skipChild:!1,inlineStylesheet:l,maskInputOptions:u===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:u===!1?{password:!0}:u,maskTextFn:p,maskInputFn:m,slimDOMOptions:f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:f==="all",headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0}:f===!1?{}:f,dataURLOptions:g,inlineImages:s,recordCanvas:c,preserveWhiteSpace:h,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:v,onStylesheetLoad:S,stylesheetLoadTimeout:b,keepIframeSrcFn:M,newlyAddedElement:!1})}function W(e,t,r=document){const n={capture:!0,passive:!0};return r.addEventListener(e,t,n),()=>r.removeEventListener(e,t,n)}const fe=`Please stop import mirror directly. Instead of that,\r +(function(){"use strict";var A;(function(e){e[e.Document=0]="Document",e[e.DocumentType=1]="DocumentType",e[e.Element=2]="Element",e[e.Text=3]="Text",e[e.CDATA=4]="CDATA",e[e.Comment=5]="Comment"})(A||(A={}));function Sr(e){return e.nodeType===e.ELEMENT_NODE}function be(e){const t=e?.host;return t?.shadowRoot===e}function we(e){return Object.prototype.toString.call(e)==="[object ShadowRoot]"}function br(e){return e.includes(" background-clip: text;")&&!e.includes(" -webkit-background-clip: text;")&&(e=e.replace(" background-clip: text;"," -webkit-background-clip: text; background-clip: text;")),e}function wr(e){const{cssText:t}=e;if(t.split('"').length<3)return t;const r=["@import",`url(${JSON.stringify(e.href)})`];return e.layerName===""?r.push("layer"):e.layerName&&r.push(`layer(${e.layerName})`),e.supportsText&&r.push(`supports(${e.supportsText})`),e.media.length&&r.push(e.media.mediaText),r.join(" ")+";"}function Te(e){try{const t=e.rules||e.cssRules;return t?br(Array.from(t,St).join("")):null}catch{return null}}function St(e){let t;if(Mr(e))try{t=Te(e.styleSheet)||wr(e)}catch{}else if(Cr(e)&&e.selectorText.includes(":"))return Ir(e.cssText);return t||e.cssText}function Ir(e){const t=/(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm;return e.replace(t,"$1\\$2")}function Mr(e){return"styleSheet"in e}function Cr(e){return"selectorText"in e}class bt{constructor(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}getId(t){var r;if(!t)return-1;const n=(r=this.getMeta(t))===null||r===void 0?void 0:r.id;return n??-1}getNode(t){return this.idNodeMap.get(t)||null}getIds(){return Array.from(this.idNodeMap.keys())}getMeta(t){return this.nodeMetaMap.get(t)||null}removeNodeFromMap(t){const r=this.getId(t);this.idNodeMap.delete(r),t.childNodes&&t.childNodes.forEach(n=>this.removeNodeFromMap(n))}has(t){return this.idNodeMap.has(t)}hasNode(t){return this.nodeMetaMap.has(t)}add(t,r){const n=r.id;this.idNodeMap.set(n,t),this.nodeMetaMap.set(t,r)}replace(t,r){const n=this.getNode(t);if(n){const i=this.nodeMetaMap.get(n);i&&this.nodeMetaMap.set(r,i)}this.idNodeMap.set(t,r)}reset(){this.idNodeMap=new Map,this.nodeMetaMap=new WeakMap}}function _r(){return new bt}function tt({element:e,maskInputOptions:t,tagName:r,type:n,value:i,maskInputFn:o}){let a=i||"";const l=n&&ie(n);return(t[r.toLowerCase()]||l&&t[l])&&(o?a=o(a,e):a="*".repeat(a.length)),a}function ie(e){return e.toLowerCase()}const wt="__rrweb_original__";function Or(e){const t=e.getContext("2d");if(!t)return!0;const r=50;for(let n=0;ns!==0))return!1}return!0}function rt(e){const t=e.type;return e.hasAttribute("data-rr-is-password")?"password":t?ie(t):null}function It(e,t){var r;let n;try{n=new URL(e,t??window.location.href)}catch{return null}const i=/\.([0-9a-z]+)(?:$)/i,o=n.pathname.match(i);return(r=o?.[1])!==null&&r!==void 0?r:null}let Er=1;const kr=new RegExp("[^a-z0-9-_:]"),Ie=-2;function Mt(){return Er++}function xr(e){if(e instanceof HTMLFormElement)return"form";const t=ie(e.tagName);return kr.test(t)?"div":t}function Rr(e){let t="";return e.indexOf("//")>-1?t=e.split("/").slice(0,3).join("/"):t=e.split("/")[0],t=t.split("?")[0],t}let fe,Ct;const Tr=/url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm,Dr=/^(?:[a-z+]+:)?\/\//i,Nr=/^www\..*/i,Ar=/^(data:)([^,]*),(.*)/i;function De(e,t){return(e||"").replace(Tr,(r,n,i,o,a,l)=>{const s=i||a||l,c=n||o||"";if(!s)return r;if(Dr.test(s)||Nr.test(s))return`url(${c}${s}${c})`;if(Ar.test(s))return`url(${c}${s}${c})`;if(s[0]==="/")return`url(${c}${Rr(t)+s}${c})`;const u=t.split("/"),f=s.split("/");u.pop();for(const m of f)m!=="."&&(m===".."?u.pop():u.push(m));return`url(${c}${u.join("/")}${c})`})}const Lr=/^[^ \t\n\r\u000c]+/,Fr=/^[, \t\n\r\u000c]+/;function Pr(e,t){if(t.trim()==="")return t;let r=0;function n(o){let a;const l=o.exec(t.substring(r));return l?(a=l[0],r+=a.length,a):""}const i=[];for(;n(Fr),!(r>=t.length);){let o=n(Lr);if(o.slice(-1)===",")o=he(e,o.substring(0,o.length-1)),i.push(o);else{let a="";o=he(e,o);let l=!1;for(;;){const s=t.charAt(r);if(s===""){i.push((o+a).trim());break}else if(l)s===")"&&(l=!1);else if(s===","){r+=1,i.push((o+a).trim());break}else s==="("&&(l=!0);a+=s,r+=1}}}return i.join(", ")}function he(e,t){if(!t||t.trim()==="")return t;const r=e.createElement("a");return r.href=t,r.href}function Br(e){return!!(e.tagName==="svg"||e.ownerSVGElement)}function nt(){const e=document.createElement("a");return e.href="",e.href}function _t(e,t,r,n){return n&&(r==="src"||r==="href"&&!(t==="use"&&n[0]==="#")||r==="xlink:href"&&n[0]!=="#"||r==="background"&&(t==="table"||t==="td"||t==="th")?he(e,n):r==="srcset"?Pr(e,n):r==="style"?De(n,nt()):t==="object"&&r==="data"?he(e,n):n)}function Ot(e,t,r){return(e==="video"||e==="audio")&&t==="autoplay"}function Wr(e,t,r){try{if(typeof t=="string"){if(e.classList.contains(t))return!0}else for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}if(r)return e.matches(r)}catch{}return!1}function Ne(e,t,r){if(!e)return!1;if(e.nodeType!==e.ELEMENT_NODE)return r?Ne(e.parentNode,t,r):!1;for(let n=e.classList.length;n--;){const i=e.classList[n];if(t.test(i))return!0}return r?Ne(e.parentNode,t,r):!1}function Et(e,t,r,n){try{const i=e.nodeType===e.ELEMENT_NODE?e:e.parentElement;if(i===null)return!1;if(typeof t=="string"){if(n){if(i.closest(`.${t}`))return!0}else if(i.classList.contains(t))return!0}else if(Ne(i,t,n))return!0;if(r){if(n){if(i.closest(r))return!0}else if(i.matches(r))return!0}}catch{}return!1}function Ur(e,t,r){const n=e.contentWindow;if(!n)return;let i=!1,o;try{o=n.document.readyState}catch{return}if(o!=="complete"){const l=setTimeout(()=>{i||(t(),i=!0)},r);e.addEventListener("load",()=>{clearTimeout(l),i=!0,t()});return}const a="about:blank";if(n.location.href!==a||e.src===a||e.src==="")return setTimeout(t,0),e.addEventListener("load",t);e.addEventListener("load",t)}function zr(e,t,r){let n=!1,i;try{i=e.sheet}catch{return}if(i)return;const o=setTimeout(()=>{n||(t(),n=!0)},r);e.addEventListener("load",()=>{clearTimeout(o),n=!0,t()})}function Hr(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:a,inlineStylesheet:l,maskInputOptions:s={},maskTextFn:c,maskInputFn:u,dataURLOptions:f={},inlineImages:m,recordCanvas:h,keepIframeSrcFn:g,newlyAddedElement:p=!1}=t,y=$r(r,n);switch(e.nodeType){case e.DOCUMENT_NODE:return e.compatMode!=="CSS1Compat"?{type:A.Document,childNodes:[],compatMode:e.compatMode}:{type:A.Document,childNodes:[]};case e.DOCUMENT_TYPE_NODE:return{type:A.DocumentType,name:e.name,publicId:e.publicId,systemId:e.systemId,rootId:y};case e.ELEMENT_NODE:return jr(e,{doc:r,blockClass:i,blockSelector:o,inlineStylesheet:l,maskInputOptions:s,maskInputFn:u,dataURLOptions:f,inlineImages:m,recordCanvas:h,keepIframeSrcFn:g,newlyAddedElement:p,rootId:y});case e.TEXT_NODE:return qr(e,{needsMask:a,maskTextFn:c,rootId:y});case e.CDATA_SECTION_NODE:return{type:A.CDATA,textContent:"",rootId:y};case e.COMMENT_NODE:return{type:A.Comment,textContent:e.textContent||"",rootId:y};default:return!1}}function $r(e,t){if(!t.hasNode(e))return;const r=t.getId(e);return r===1?void 0:r}function qr(e,t){var r;const{needsMask:n,maskTextFn:i,rootId:o}=t,a=e.parentNode&&e.parentNode.tagName;let l=e.textContent;const s=a==="STYLE"?!0:void 0,c=a==="SCRIPT"?!0:void 0;if(s&&l){try{e.nextSibling||e.previousSibling||!((r=e.parentNode.sheet)===null||r===void 0)&&r.cssRules&&(l=Te(e.parentNode.sheet))}catch(u){console.warn(`Cannot get CSS styles from text's parentNode. Error: ${u}`,e)}l=De(l,nt())}return c&&(l="SCRIPT_PLACEHOLDER"),!s&&!c&&l&&n&&(l=i?i(l,e.parentElement):l.replace(/[\S]/g,"*")),{type:A.Text,textContent:l||"",isStyle:s,rootId:o}}function jr(e,t){const{doc:r,blockClass:n,blockSelector:i,inlineStylesheet:o,maskInputOptions:a={},maskInputFn:l,dataURLOptions:s={},inlineImages:c,recordCanvas:u,keepIframeSrcFn:f,newlyAddedElement:m=!1,rootId:h}=t,g=Wr(e,n,i),p=xr(e);let y={};const w=e.attributes.length;for(let v=0;vI.href===e.href);let b=null;v&&(b=Te(v)),b&&(delete y.rel,delete y.href,y._cssText=De(b,v.href))}if(p==="style"&&e.sheet&&!(e.innerText||e.textContent||"").trim().length){const v=Te(e.sheet);v&&(y._cssText=De(v,nt()))}if(p==="input"||p==="textarea"||p==="select"){const v=e.value,b=e.checked;y.type!=="radio"&&y.type!=="checkbox"&&y.type!=="submit"&&y.type!=="button"&&v?y.value=tt({element:e,type:rt(e),tagName:p,value:v,maskInputOptions:a,maskInputFn:l}):b&&(y.checked=b)}if(p==="option"&&(e.selected&&!a.select?y.selected=!0:delete y.selected),p==="canvas"&&u){if(e.__context==="2d")Or(e)||(y.rr_dataURL=e.toDataURL(s.type,s.quality));else if(!("__context"in e)){const v=e.toDataURL(s.type,s.quality),b=document.createElement("canvas");b.width=e.width,b.height=e.height;const I=b.toDataURL(s.type,s.quality);v!==I&&(y.rr_dataURL=v)}}if(p==="img"&&c){fe||(fe=r.createElement("canvas"),Ct=fe.getContext("2d"));const v=e,b=v.crossOrigin;v.crossOrigin="anonymous";const I=()=>{v.removeEventListener("load",I);try{fe.width=v.naturalWidth,fe.height=v.naturalHeight,Ct.drawImage(v,0,0),y.rr_dataURL=fe.toDataURL(s.type,s.quality)}catch(B){console.warn(`Cannot inline img src=${v.currentSrc}! Error: ${B}`)}b?y.crossOrigin=b:v.removeAttribute("crossorigin")};v.complete&&v.naturalWidth!==0?I():v.addEventListener("load",I)}if(p==="audio"||p==="video"){const v=y;v.rr_mediaState=e.paused?"paused":"played",v.rr_mediaCurrentTime=e.currentTime,v.rr_mediaPlaybackRate=e.playbackRate,v.rr_mediaMuted=e.muted,v.rr_mediaLoop=e.loop,v.rr_mediaVolume=e.volume}if(m||(e.scrollLeft&&(y.rr_scrollLeft=e.scrollLeft),e.scrollTop&&(y.rr_scrollTop=e.scrollTop)),g){const{width:v,height:b}=e.getBoundingClientRect();y={class:y.class,rr_width:`${v}px`,rr_height:`${b}px`}}p==="iframe"&&!f(y.src)&&(e.contentDocument||(y.rr_src=y.src),delete y.src);let S;try{customElements.get(p)&&(S=!0)}catch{}return{type:A.Element,tagName:p,attributes:y,childNodes:[],isSVG:Br(e)||void 0,needBlock:g,rootId:h,isCustom:S}}function k(e){return e==null?"":e.toLowerCase()}function Gr(e,t){if(t.comment&&e.type===A.Comment)return!0;if(e.type===A.Element){if(t.script&&(e.tagName==="script"||e.tagName==="link"&&(e.attributes.rel==="preload"||e.attributes.rel==="modulepreload")&&e.attributes.as==="script"||e.tagName==="link"&&e.attributes.rel==="prefetch"&&typeof e.attributes.href=="string"&&It(e.attributes.href)==="js"))return!0;if(t.headFavicon&&(e.tagName==="link"&&e.attributes.rel==="shortcut icon"||e.tagName==="meta"&&(k(e.attributes.name).match(/^msapplication-tile(image|color)$/)||k(e.attributes.name)==="application-name"||k(e.attributes.rel)==="icon"||k(e.attributes.rel)==="apple-touch-icon"||k(e.attributes.rel)==="shortcut icon")))return!0;if(e.tagName==="meta"){if(t.headMetaDescKeywords&&k(e.attributes.name).match(/^description|keywords$/))return!0;if(t.headMetaSocial&&(k(e.attributes.property).match(/^(og|twitter|fb):/)||k(e.attributes.name).match(/^(og|twitter):/)||k(e.attributes.name)==="pinterest"))return!0;if(t.headMetaRobots&&(k(e.attributes.name)==="robots"||k(e.attributes.name)==="googlebot"||k(e.attributes.name)==="bingbot"))return!0;if(t.headMetaHttpEquiv&&e.attributes["http-equiv"]!==void 0)return!0;if(t.headMetaAuthorship&&(k(e.attributes.name)==="author"||k(e.attributes.name)==="generator"||k(e.attributes.name)==="framework"||k(e.attributes.name)==="publisher"||k(e.attributes.name)==="progid"||k(e.attributes.property).match(/^article:/)||k(e.attributes.property).match(/^product:/)))return!0;if(t.headMetaVerification&&(k(e.attributes.name)==="google-site-verification"||k(e.attributes.name)==="yandex-verification"||k(e.attributes.name)==="csrf-token"||k(e.attributes.name)==="p:domain_verify"||k(e.attributes.name)==="verify-v1"||k(e.attributes.name)==="verification"||k(e.attributes.name)==="shopify-checkout-api-token"))return!0}}return!1}function pe(e,t){const{doc:r,mirror:n,blockClass:i,blockSelector:o,maskTextClass:a,maskTextSelector:l,skipChild:s=!1,inlineStylesheet:c=!0,maskInputOptions:u={},maskTextFn:f,maskInputFn:m,slimDOMOptions:h,dataURLOptions:g={},inlineImages:p=!1,recordCanvas:y=!1,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v=5e3,onStylesheetLoad:b,stylesheetLoadTimeout:I=5e3,keepIframeSrcFn:B=()=>!1,newlyAddedElement:F=!1}=t;let{needsMask:x}=t,{preserveWhiteSpace:R=!0}=t;!x&&e.childNodes&&(x=Et(e,a,l,x===void 0));const j=Hr(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:x,inlineStylesheet:c,maskInputOptions:u,maskTextFn:f,maskInputFn:m,dataURLOptions:g,inlineImages:p,recordCanvas:y,keepIframeSrcFn:B,newlyAddedElement:F});if(!j)return console.warn(e,"not serialized"),null;let G;n.hasNode(e)?G=n.getId(e):Gr(j,h)||!R&&j.type===A.Text&&!j.isStyle&&!j.textContent.replace(/^\s+|\s+$/gm,"").length?G=Ie:G=Mt();const E=Object.assign(j,{id:G});if(n.add(e,E),G===Ie)return null;w&&w(e);let le=!s;if(E.type===A.Element){le=le&&!E.needBlock,delete E.needBlock;const z=e.shadowRoot;z&&we(z)&&(E.isShadowHost=!0)}if((E.type===A.Document||E.type===A.Element)&&le){h.headWhitespace&&E.type===A.Element&&E.tagName==="head"&&(R=!1);const z={doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:x,maskTextClass:a,maskTextSelector:l,skipChild:s,inlineStylesheet:c,maskInputOptions:u,maskTextFn:f,maskInputFn:m,slimDOMOptions:h,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:R,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B};if(!(E.type===A.Element&&E.tagName==="textarea"&&E.attributes.value!==void 0))for(const ne of Array.from(e.childNodes)){const Z=pe(ne,z);Z&&E.childNodes.push(Z)}if(Sr(e)&&e.shadowRoot)for(const ne of Array.from(e.shadowRoot.childNodes)){const Z=pe(ne,z);Z&&(we(e.shadowRoot)&&(Z.isShadow=!0),E.childNodes.push(Z))}}return e.parentNode&&be(e.parentNode)&&we(e.parentNode)&&(E.isShadow=!0),E.type===A.Element&&E.tagName==="iframe"&&Ur(e,()=>{const z=e.contentDocument;if(z&&S){const ne=pe(z,{doc:z,mirror:n,blockClass:i,blockSelector:o,needsMask:x,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:f,maskInputFn:m,slimDOMOptions:h,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:R,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B});ne&&S(e,ne)}},v),E.type===A.Element&&E.tagName==="link"&&typeof E.attributes.rel=="string"&&(E.attributes.rel==="stylesheet"||E.attributes.rel==="preload"&&typeof E.attributes.href=="string"&&It(E.attributes.href)==="css")&&zr(e,()=>{if(b){const z=pe(e,{doc:r,mirror:n,blockClass:i,blockSelector:o,needsMask:x,maskTextClass:a,maskTextSelector:l,skipChild:!1,inlineStylesheet:c,maskInputOptions:u,maskTextFn:f,maskInputFn:m,slimDOMOptions:h,dataURLOptions:g,inlineImages:p,recordCanvas:y,preserveWhiteSpace:R,onSerialize:w,onIframeLoad:S,iframeLoadTimeout:v,onStylesheetLoad:b,stylesheetLoadTimeout:I,keepIframeSrcFn:B});z&&b(e,z)}},I),E}function Vr(e,t){const{mirror:r=new bt,blockClass:n="rr-block",blockSelector:i=null,maskTextClass:o="rr-mask",maskTextSelector:a=null,inlineStylesheet:l=!0,inlineImages:s=!1,recordCanvas:c=!1,maskAllInputs:u=!1,maskTextFn:f,maskInputFn:m,slimDOM:h=!1,dataURLOptions:g,preserveWhiteSpace:p,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:S,onStylesheetLoad:v,stylesheetLoadTimeout:b,keepIframeSrcFn:I=()=>!1}=t||{};return pe(e,{doc:e,mirror:r,blockClass:n,blockSelector:i,maskTextClass:o,maskTextSelector:a,skipChild:!1,inlineStylesheet:l,maskInputOptions:u===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:u===!1?{password:!0}:u,maskTextFn:f,maskInputFn:m,slimDOMOptions:h===!0||h==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:h==="all",headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0,headMetaVerification:!0}:h===!1?{}:h,dataURLOptions:g,inlineImages:s,recordCanvas:c,preserveWhiteSpace:p,onSerialize:y,onIframeLoad:w,iframeLoadTimeout:S,onStylesheetLoad:v,stylesheetLoadTimeout:b,keepIframeSrcFn:I,newlyAddedElement:!1})}function W(e,t,r=document){const n={capture:!0,passive:!0};return r.addEventListener(e,t,n),()=>r.removeEventListener(e,t,n)}const me=`Please stop import mirror directly. Instead of that,\r now you can use replayer.getMirror() to access the mirror instance of a replayer,\r -or you can use record.mirror to access the mirror instance during recording.`;let wt={map:{},getId(){return console.error(fe),-1},getNode(){return console.error(fe),null},removeNodeFromMap(){console.error(fe)},has(){return console.error(fe),!1},reset(){console.error(fe)}};typeof window<"u"&&window.Proxy&&window.Reflect&&(wt=new Proxy(wt,{get(e,t,r){return t==="map"&&console.error(fe),Reflect.get(e,t,r)}}));function ve(e,t,r={}){let n=null,i=0;return function(...o){const a=Date.now();!i&&r.leading===!1&&(i=a);const l=t-(a-i),s=this;l<=0||l>t?(n&&(clearTimeout(n),n=null),i=a,e.apply(s,o)):!n&&r.trailing!==!1&&(n=setTimeout(()=>{i=r.leading===!1?0:Date.now(),n=null,e.apply(s,o)},l))}}function ke(e,t,r,n,i=window){const o=i.Object.getOwnPropertyDescriptor(e,t);return i.Object.defineProperty(e,t,n?r:{set(a){setTimeout(()=>{r.set.call(this,a)},0),o&&o.set&&o.set.call(this,a)}}),()=>ke(e,t,o||{},!0)}function he(e,t,r){try{if(!(t in e))return()=>{};const n=e[t],i=r(n);return typeof i=="function"&&(i.prototype=i.prototype||{},Object.defineProperties(i,{__rrweb_original__:{enumerable:!1,value:n}})),e[t]=i,()=>{e[t]=n}}catch{return()=>{}}}let Te=Date.now;/[1-9][0-9]{12}/.test(Date.now().toString())||(Te=()=>new Date().getTime());function Mt(e){var t,r,n,i,o,a;const l=e.document;return{left:l.scrollingElement?l.scrollingElement.scrollLeft:e.pageXOffset!==void 0?e.pageXOffset:l?.documentElement.scrollLeft||((r=(t=l?.body)===null||t===void 0?void 0:t.parentElement)===null||r===void 0?void 0:r.scrollLeft)||((n=l?.body)===null||n===void 0?void 0:n.scrollLeft)||0,top:l.scrollingElement?l.scrollingElement.scrollTop:e.pageYOffset!==void 0?e.pageYOffset:l?.documentElement.scrollTop||((o=(i=l?.body)===null||i===void 0?void 0:i.parentElement)===null||o===void 0?void 0:o.scrollTop)||((a=l?.body)===null||a===void 0?void 0:a.scrollTop)||0}}function It(){return window.innerHeight||document.documentElement&&document.documentElement.clientHeight||document.body&&document.body.clientHeight}function Ct(){return window.innerWidth||document.documentElement&&document.documentElement.clientWidth||document.body&&document.body.clientWidth}function Ot(e){return e?e.nodeType===e.ELEMENT_NODE?e:e.parentElement:null}function U(e,t,r,n){if(!e)return!1;const i=Ot(e);if(!i)return!1;try{if(typeof t=="string"){if(i.classList.contains(t)||n&&i.closest("."+t)!==null)return!0}else if(Ee(i,t,n))return!0}catch{}return!!(r&&(i.matches(r)||n&&i.closest(r)!==null))}function Pr(e,t){return t.getId(e)!==-1}function Xe(e,t){return t.getId(e)===Se}function _t(e,t){if(ge(e))return!1;const r=t.getId(e);return t.has(r)?e.parentNode&&e.parentNode.nodeType===e.DOCUMENT_NODE?!1:e.parentNode?_t(e.parentNode,t):!0:!0}function Ke(e){return!!e.changedTouches}function Fr(e=window){"NodeList"in e&&!e.NodeList.prototype.forEach&&(e.NodeList.prototype.forEach=Array.prototype.forEach),"DOMTokenList"in e&&!e.DOMTokenList.prototype.forEach&&(e.DOMTokenList.prototype.forEach=Array.prototype.forEach),Node.prototype.contains||(Node.prototype.contains=(...t)=>{let r=t[0];if(!(0 in t))throw new TypeError("1 argument is required");do if(this===r)return!0;while(r=r&&r.parentNode);return!1})}function xt(e,t){return!!(e.nodeName==="IFRAME"&&t.getMeta(e))}function Et(e,t){return!!(e.nodeName==="LINK"&&e.nodeType===e.ELEMENT_NODE&&e.getAttribute&&e.getAttribute("rel")==="stylesheet"&&t.getMeta(e))}function Ye(e){return!!e?.shadowRoot}class Br{constructor(){this.id=1,this.styleIDMap=new WeakMap,this.idStyleMap=new Map}getId(t){var r;return(r=this.styleIDMap.get(t))!==null&&r!==void 0?r:-1}has(t){return this.styleIDMap.has(t)}add(t,r){if(this.has(t))return this.getId(t);let n;return r===void 0?n=this.id++:n=r,this.styleIDMap.set(t,n),this.idStyleMap.set(n,t),n}getStyle(t){return this.idStyleMap.get(t)||null}reset(){this.styleIDMap=new WeakMap,this.idStyleMap=new Map,this.id=1}generateId(){return this.id++}}function kt(e){var t,r;let n=null;return((r=(t=e.getRootNode)===null||t===void 0?void 0:t.call(e))===null||r===void 0?void 0:r.nodeType)===Node.DOCUMENT_FRAGMENT_NODE&&e.getRootNode().host&&(n=e.getRootNode().host),n}function Wr(e){let t=e,r;for(;r=kt(t);)t=r;return t}function Ur(e){const t=e.ownerDocument;if(!t)return!1;const r=Wr(e);return t.contains(r)}function Tt(e){const t=e.ownerDocument;return t?t.contains(e)||Ur(e):!1}var _=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(_||{}),C=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(C||{}),$=(e=>(e[e.MouseUp=0]="MouseUp",e[e.MouseDown=1]="MouseDown",e[e.Click=2]="Click",e[e.ContextMenu=3]="ContextMenu",e[e.DblClick=4]="DblClick",e[e.Focus=5]="Focus",e[e.Blur=6]="Blur",e[e.TouchStart=7]="TouchStart",e[e.TouchMove_Departed=8]="TouchMove_Departed",e[e.TouchEnd=9]="TouchEnd",e[e.TouchCancel=10]="TouchCancel",e))($||{}),Y=(e=>(e[e.Mouse=0]="Mouse",e[e.Pen=1]="Pen",e[e.Touch=2]="Touch",e))(Y||{}),pe=(e=>(e[e["2D"]=0]="2D",e[e.WebGL=1]="WebGL",e[e.WebGL2=2]="WebGL2",e))(pe||{});function Rt(e){return"__ln"in e}class Hr{constructor(){this.length=0,this.head=null,this.tail=null}get(t){if(t>=this.length)throw new Error("Position outside of list range");let r=this.head;for(let n=0;n`${e}@${t}`;class zr{constructor(){this.frozen=!1,this.locked=!1,this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.mapRemoves=[],this.movedMap={},this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.processMutations=t=>{t.forEach(this.processMutation),this.emit()},this.emit=()=>{if(this.frozen||this.locked)return;const t=[],r=new Set,n=new Hr,i=s=>{let c=s,u=Se;for(;u===Se;)c=c&&c.nextSibling,u=c&&this.mirror.getId(c);return u},o=s=>{if(!s.parentNode||!Tt(s)||s.parentNode.tagName==="TEXTAREA")return;const c=ge(s.parentNode)?this.mirror.getId(kt(s)):this.mirror.getId(s.parentNode),u=i(s);if(c===-1||u===-1)return n.addNode(s);const p=de(s,{doc:this.doc,mirror:this.mirror,blockClass:this.blockClass,blockSelector:this.blockSelector,maskTextClass:this.maskTextClass,maskTextSelector:this.maskTextSelector,skipChild:!0,newlyAddedElement:!0,inlineStylesheet:this.inlineStylesheet,maskInputOptions:this.maskInputOptions,maskTextFn:this.maskTextFn,maskInputFn:this.maskInputFn,slimDOMOptions:this.slimDOMOptions,dataURLOptions:this.dataURLOptions,recordCanvas:this.recordCanvas,inlineImages:this.inlineImages,onSerialize:m=>{xt(m,this.mirror)&&this.iframeManager.addIframe(m),Et(m,this.mirror)&&this.stylesheetManager.trackLinkElement(m),Ye(s)&&this.shadowDomManager.addShadowRoot(s.shadowRoot,this.doc)},onIframeLoad:(m,f)=>{this.iframeManager.attachIframe(m,f),this.shadowDomManager.observeAttachShadow(m)},onStylesheetLoad:(m,f)=>{this.stylesheetManager.attachLinkElement(m,f)}});p&&(t.push({parentId:c,nextId:u,node:p}),r.add(p.id))};for(;this.mapRemoves.length;)this.mirror.removeNodeFromMap(this.mapRemoves.shift());for(const s of this.movedSet)Dt(this.removes,s,this.mirror)&&!this.movedSet.has(s.parentNode)||o(s);for(const s of this.addedSet)!Lt(this.droppedSet,s)&&!Dt(this.removes,s,this.mirror)||Lt(this.movedSet,s)?o(s):this.droppedSet.add(s);let a=null;for(;n.length;){let s=null;if(a){const c=this.mirror.getId(a.value.parentNode),u=i(a.value);c!==-1&&u!==-1&&(s=a)}if(!s){let c=n.tail;for(;c;){const u=c;if(c=c.previous,u){const p=this.mirror.getId(u.value.parentNode);if(i(u.value)===-1)continue;if(p!==-1){s=u;break}else{const f=u.value;if(f.parentNode&&f.parentNode.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const g=f.parentNode.host;if(this.mirror.getId(g)!==-1){s=u;break}}}}}}if(!s){for(;n.head;)n.removeNode(n.head.value);break}a=s.previous,n.removeNode(s.value),o(s.value)}const l={texts:this.texts.map(s=>{const c=s.node;return c.parentNode&&c.parentNode.tagName==="TEXTAREA"&&this.genTextAreaValueMutation(c.parentNode),{id:this.mirror.getId(c),value:s.value}}).filter(s=>!r.has(s.id)).filter(s=>this.mirror.has(s.id)),attributes:this.attributes.map(s=>{const{attributes:c}=s;if(typeof c.style=="string"){const u=JSON.stringify(s.styleDiff),p=JSON.stringify(s._unchangedStyles);u.length!r.has(s.id)).filter(s=>this.mirror.has(s.id)),removes:this.removes,adds:t};!l.texts.length&&!l.attributes.length&&!l.removes.length&&!l.adds.length||(this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.movedMap={},this.mutationCb(l))},this.genTextAreaValueMutation=t=>{let r=this.attributeMap.get(t);r||(r={node:t,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(r),this.attributeMap.set(t,r)),r.attributes.value=Array.from(t.childNodes,n=>n.textContent||"").join("")},this.processMutation=t=>{if(!Xe(t.target,this.mirror))switch(t.type){case"characterData":{const r=t.target.textContent;!U(t.target,this.blockClass,this.blockSelector,!1)&&r!==t.oldValue&&this.texts.push({value:bt(t.target,this.maskTextClass,this.maskTextSelector,!0)&&r?this.maskTextFn?this.maskTextFn(r,Ot(t.target)):r.replace(/[\S]/g,"*"):r,node:t.target});break}case"attributes":{const r=t.target;let n=t.attributeName,i=t.target.getAttribute(n);if(n==="value"){const a=qe(r);i=Ve({element:r,maskInputOptions:this.maskInputOptions,tagName:r.tagName,type:a,value:i,maskInputFn:this.maskInputFn})}if(U(t.target,this.blockClass,this.blockSelector,!1)||i===t.oldValue)return;let o=this.attributeMap.get(t.target);if(r.tagName==="IFRAME"&&n==="src"&&!this.keepIframeSrcFn(i))if(!r.contentDocument)n="rr_src";else return;if(o||(o={node:t.target,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(o),this.attributeMap.set(t.target,o)),n==="type"&&r.tagName==="INPUT"&&(t.oldValue||"").toLowerCase()==="password"&&r.setAttribute("data-rr-is-password","true"),!vt(r.tagName,n)&&(o.attributes[n]=St(this.doc,te(r.tagName),te(n),i),n==="style")){if(!this.unattachedDoc)try{this.unattachedDoc=document.implementation.createHTMLDocument()}catch{this.unattachedDoc=this.doc}const a=this.unattachedDoc.createElement("span");t.oldValue&&a.setAttribute("style",t.oldValue);for(const l of Array.from(r.style)){const s=r.style.getPropertyValue(l),c=r.style.getPropertyPriority(l);s!==a.style.getPropertyValue(l)||c!==a.style.getPropertyPriority(l)?c===""?o.styleDiff[l]=s:o.styleDiff[l]=[s,c]:o._unchangedStyles[l]=[s,c]}for(const l of Array.from(a.style))r.style.getPropertyValue(l)===""&&(o.styleDiff[l]=!1)}break}case"childList":{if(U(t.target,this.blockClass,this.blockSelector,!0))return;if(t.target.tagName==="TEXTAREA"){this.genTextAreaValueMutation(t.target);return}t.addedNodes.forEach(r=>this.genAdds(r,t.target)),t.removedNodes.forEach(r=>{const n=this.mirror.getId(r),i=ge(t.target)?this.mirror.getId(t.target.host):this.mirror.getId(t.target);U(t.target,this.blockClass,this.blockSelector,!1)||Xe(r,this.mirror)||!Pr(r,this.mirror)||(this.addedSet.has(r)?(Qe(this.addedSet,r),this.droppedSet.add(r)):this.addedSet.has(t.target)&&n===-1||_t(t.target,this.mirror)||(this.movedSet.has(r)&&this.movedMap[Nt(n,i)]?Qe(this.movedSet,r):this.removes.push({parentId:i,id:n,isShadow:ge(t.target)&&ye(t.target)?!0:void 0})),this.mapRemoves.push(r))});break}}},this.genAdds=(t,r)=>{if(!this.processedNodeManager.inOtherBuffer(t,this)&&!(this.addedSet.has(t)||this.movedSet.has(t))){if(this.mirror.hasNode(t)){if(Xe(t,this.mirror))return;this.movedSet.add(t);let n=null;r&&this.mirror.hasNode(r)&&(n=this.mirror.getId(r)),n&&n!==-1&&(this.movedMap[Nt(this.mirror.getId(t),n)]=!0)}else this.addedSet.add(t),this.droppedSet.delete(t);U(t,this.blockClass,this.blockSelector,!1)||(t.childNodes.forEach(n=>this.genAdds(n)),Ye(t)&&t.shadowRoot.childNodes.forEach(n=>{this.processedNodeManager.add(n,this),this.genAdds(n,t)}))}}}init(t){["mutationCb","blockClass","blockSelector","maskTextClass","maskTextSelector","inlineStylesheet","maskInputOptions","maskTextFn","maskInputFn","keepIframeSrcFn","recordCanvas","inlineImages","slimDOMOptions","dataURLOptions","doc","mirror","iframeManager","stylesheetManager","shadowDomManager","canvasManager","processedNodeManager"].forEach(r=>{this[r]=t[r]})}freeze(){this.frozen=!0,this.canvasManager.freeze()}unfreeze(){this.frozen=!1,this.canvasManager.unfreeze(),this.emit()}isFrozen(){return this.frozen}lock(){this.locked=!0,this.canvasManager.lock()}unlock(){this.locked=!1,this.canvasManager.unlock(),this.emit()}reset(){this.shadowDomManager.reset(),this.canvasManager.reset()}}function Qe(e,t){e.delete(t),t.childNodes.forEach(r=>Qe(e,r))}function Dt(e,t,r){return e.length===0?!1:At(e,t,r)}function At(e,t,r){const{parentNode:n}=t;if(!n)return!1;const i=r.getId(n);return e.some(o=>o.id===i)?!0:At(e,n,r)}function Lt(e,t){return e.size===0?!1:Pt(e,t)}function Pt(e,t){const{parentNode:r}=t;return r?e.has(r)?!0:Pt(e,r):!1}let be;function $r(e){be=e}function Gr(){be=void 0}const O=e=>be?(...r)=>{try{return e(...r)}catch(n){if(be&&be(n)===!0)return;throw n}}:e,re=[];function we(e){try{if("composedPath"in e){const t=e.composedPath();if(t.length)return t[0]}else if("path"in e&&e.path.length)return e.path[0]}catch{}return e&&e.target}function Ft(e,t){var r,n;const i=new zr;re.push(i),i.init(e);let o=window.MutationObserver||window.__rrMutationObserver;const a=(n=(r=window?.Zone)===null||r===void 0?void 0:r.__symbol__)===null||n===void 0?void 0:n.call(r,"MutationObserver");a&&window[a]&&(o=window[a]);const l=new o(O(i.processMutations.bind(i)));return l.observe(t,{attributes:!0,attributeOldValue:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0}),l}function jr({mousemoveCb:e,sampling:t,doc:r,mirror:n}){if(t.mousemove===!1)return()=>{};const i=typeof t.mousemove=="number"?t.mousemove:50,o=typeof t.mousemoveCallback=="number"?t.mousemoveCallback:500;let a=[],l;const s=ve(O(p=>{const m=Date.now()-l;e(a.map(f=>(f.timeOffset-=m,f)),p),a=[],l=null}),o),c=O(ve(O(p=>{const m=we(p),{clientX:f,clientY:g}=Ke(p)?p.changedTouches[0]:p;l||(l=Te()),a.push({x:f,y:g,id:n.getId(m),timeOffset:Te()-l}),s(typeof DragEvent<"u"&&p instanceof DragEvent?C.Drag:p instanceof MouseEvent?C.MouseMove:C.TouchMove)}),i,{trailing:!1})),u=[W("mousemove",c,r),W("touchmove",c,r),W("drag",c,r)];return O(()=>{u.forEach(p=>p())})}function Vr({mouseInteractionCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){if(o.mouseInteraction===!1)return()=>{};const a=o.mouseInteraction===!0||o.mouseInteraction===void 0?{}:o.mouseInteraction,l=[];let s=null;const c=u=>p=>{const m=we(p);if(U(m,n,i,!0))return;let f=null,g=u;if("pointerType"in p){switch(p.pointerType){case"mouse":f=Y.Mouse;break;case"touch":f=Y.Touch;break;case"pen":f=Y.Pen;break}f===Y.Touch?$[u]===$.MouseDown?g="TouchStart":$[u]===$.MouseUp&&(g="TouchEnd"):Y.Pen}else Ke(p)&&(f=Y.Touch);f!==null?(s=f,(g.startsWith("Touch")&&f===Y.Touch||g.startsWith("Mouse")&&f===Y.Mouse)&&(f=null)):$[u]===$.Click&&(f=s,s=null);const h=Ke(p)?p.changedTouches[0]:p;if(!h)return;const y=r.getId(m),{clientX:w,clientY:v}=h;O(e)(Object.assign({type:$[g],id:y,x:w,y:v},f!==null&&{pointerType:f}))};return Object.keys($).filter(u=>Number.isNaN(Number(u))&&!u.endsWith("_Departed")&&a[u]!==!1).forEach(u=>{let p=te(u);const m=c(u);if(window.PointerEvent)switch($[u]){case $.MouseDown:case $.MouseUp:p=p.replace("mouse","pointer");break;case $.TouchStart:case $.TouchEnd:return}l.push(W(p,m,t))}),O(()=>{l.forEach(u=>u())})}function Bt({scrollCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){const a=O(ve(O(l=>{const s=we(l);if(!s||U(s,n,i,!0))return;const c=r.getId(s);if(s===t&&t.defaultView){const u=Mt(t.defaultView);e({id:c,x:u.left,y:u.top})}else e({id:c,x:s.scrollLeft,y:s.scrollTop})}),o.scroll||100));return W("scroll",a,t)}function qr({viewportResizeCb:e},{win:t}){let r=-1,n=-1;const i=O(ve(O(()=>{const o=It(),a=Ct();(r!==o||n!==a)&&(e({width:Number(a),height:Number(o)}),r=o,n=a)}),200));return W("resize",i,t)}const Jr=["INPUT","TEXTAREA","SELECT"],Wt=new WeakMap;function Xr({inputCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,ignoreClass:o,ignoreSelector:a,maskInputOptions:l,maskInputFn:s,sampling:c,userTriggeredOnInput:u}){function p(v){let S=we(v);const b=v.isTrusted,M=S&&S.tagName;if(S&&M==="OPTION"&&(S=S.parentElement),!S||!M||Jr.indexOf(M)<0||U(S,n,i,!0)||S.classList.contains(o)||a&&S.matches(a))return;let F=S.value,P=!1;const k=qe(S)||"";k==="radio"||k==="checkbox"?P=S.checked:(l[M.toLowerCase()]||l[k])&&(F=Ve({element:S,maskInputOptions:l,tagName:M,type:k,value:F,maskInputFn:s})),m(S,u?{text:F,isChecked:P,userTriggered:b}:{text:F,isChecked:P});const T=S.name;k==="radio"&&T&&P&&t.querySelectorAll(`input[type="radio"][name="${T}"]`).forEach(j=>{if(j!==S){const V=j.value;m(j,u?{text:V,isChecked:!P,userTriggered:!1}:{text:V,isChecked:!P})}})}function m(v,S){const b=Wt.get(v);if(!b||b.text!==S.text||b.isChecked!==S.isChecked){Wt.set(v,S);const M=r.getId(v);O(e)(Object.assign(Object.assign({},S),{id:M}))}}const g=(c.input==="last"?["change"]:["input","change"]).map(v=>W(v,O(p),t)),h=t.defaultView;if(!h)return()=>{g.forEach(v=>v())};const y=h.Object.getOwnPropertyDescriptor(h.HTMLInputElement.prototype,"value"),w=[[h.HTMLInputElement.prototype,"value"],[h.HTMLInputElement.prototype,"checked"],[h.HTMLSelectElement.prototype,"value"],[h.HTMLTextAreaElement.prototype,"value"],[h.HTMLSelectElement.prototype,"selectedIndex"],[h.HTMLOptionElement.prototype,"selected"]];return y&&y.set&&g.push(...w.map(v=>ke(v[0],v[1],{set(){O(p)({target:this,isTrusted:!1})}},!1,h))),O(()=>{g.forEach(v=>v())})}function Re(e){const t=[];function r(n,i){if(Ne("CSSGroupingRule")&&n.parentRule instanceof CSSGroupingRule||Ne("CSSMediaRule")&&n.parentRule instanceof CSSMediaRule||Ne("CSSSupportsRule")&&n.parentRule instanceof CSSSupportsRule||Ne("CSSConditionRule")&&n.parentRule instanceof CSSConditionRule){const a=Array.from(n.parentRule.cssRules).indexOf(n);i.unshift(a)}else if(n.parentStyleSheet){const a=Array.from(n.parentStyleSheet.cssRules).indexOf(n);i.unshift(a)}return i}return r(e,t)}function Z(e,t,r){let n,i;return e?(e.ownerNode?n=t.getId(e.ownerNode):i=r.getId(e),{styleId:i,id:n}):{}}function Kr({styleSheetRuleCb:e,mirror:t,stylesheetManager:r},{win:n}){if(!n.CSSStyleSheet||!n.CSSStyleSheet.prototype)return()=>{};const i=n.CSSStyleSheet.prototype.insertRule;n.CSSStyleSheet.prototype.insertRule=new Proxy(i,{apply:O((u,p,m)=>{const[f,g]=m,{id:h,styleId:y}=Z(p,t,r.styleMirror);return(h&&h!==-1||y&&y!==-1)&&e({id:h,styleId:y,adds:[{rule:f,index:g}]}),u.apply(p,m)})});const o=n.CSSStyleSheet.prototype.deleteRule;n.CSSStyleSheet.prototype.deleteRule=new Proxy(o,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,removes:[{index:f}]}),u.apply(p,m)})});let a;n.CSSStyleSheet.prototype.replace&&(a=n.CSSStyleSheet.prototype.replace,n.CSSStyleSheet.prototype.replace=new Proxy(a,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,replace:f}),u.apply(p,m)})}));let l;n.CSSStyleSheet.prototype.replaceSync&&(l=n.CSSStyleSheet.prototype.replaceSync,n.CSSStyleSheet.prototype.replaceSync=new Proxy(l,{apply:O((u,p,m)=>{const[f]=m,{id:g,styleId:h}=Z(p,t,r.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,replaceSync:f}),u.apply(p,m)})}));const s={};De("CSSGroupingRule")?s.CSSGroupingRule=n.CSSGroupingRule:(De("CSSMediaRule")&&(s.CSSMediaRule=n.CSSMediaRule),De("CSSConditionRule")&&(s.CSSConditionRule=n.CSSConditionRule),De("CSSSupportsRule")&&(s.CSSSupportsRule=n.CSSSupportsRule));const c={};return Object.entries(s).forEach(([u,p])=>{c[u]={insertRule:p.prototype.insertRule,deleteRule:p.prototype.deleteRule},p.prototype.insertRule=new Proxy(c[u].insertRule,{apply:O((m,f,g)=>{const[h,y]=g,{id:w,styleId:v}=Z(f.parentStyleSheet,t,r.styleMirror);return(w&&w!==-1||v&&v!==-1)&&e({id:w,styleId:v,adds:[{rule:h,index:[...Re(f),y||0]}]}),m.apply(f,g)})}),p.prototype.deleteRule=new Proxy(c[u].deleteRule,{apply:O((m,f,g)=>{const[h]=g,{id:y,styleId:w}=Z(f.parentStyleSheet,t,r.styleMirror);return(y&&y!==-1||w&&w!==-1)&&e({id:y,styleId:w,removes:[{index:[...Re(f),h]}]}),m.apply(f,g)})})}),O(()=>{n.CSSStyleSheet.prototype.insertRule=i,n.CSSStyleSheet.prototype.deleteRule=o,a&&(n.CSSStyleSheet.prototype.replace=a),l&&(n.CSSStyleSheet.prototype.replaceSync=l),Object.entries(s).forEach(([u,p])=>{p.prototype.insertRule=c[u].insertRule,p.prototype.deleteRule=c[u].deleteRule})})}function Ut({mirror:e,stylesheetManager:t},r){var n,i,o;let a=null;r.nodeName==="#document"?a=e.getId(r):a=e.getId(r.host);const l=r.nodeName==="#document"?(n=r.defaultView)===null||n===void 0?void 0:n.Document:(o=(i=r.ownerDocument)===null||i===void 0?void 0:i.defaultView)===null||o===void 0?void 0:o.ShadowRoot,s=l?.prototype?Object.getOwnPropertyDescriptor(l?.prototype,"adoptedStyleSheets"):void 0;return a===null||a===-1||!l||!s?()=>{}:(Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get(){var c;return(c=s.get)===null||c===void 0?void 0:c.call(this)},set(c){var u;const p=(u=s.set)===null||u===void 0?void 0:u.call(this,c);if(a!==null&&a!==-1)try{t.adoptStyleSheets(c,a)}catch{}return p}}),O(()=>{Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get:s.get,set:s.set})}))}function Yr({styleDeclarationCb:e,mirror:t,ignoreCSSAttributes:r,stylesheetManager:n},{win:i}){const o=i.CSSStyleDeclaration.prototype.setProperty;i.CSSStyleDeclaration.prototype.setProperty=new Proxy(o,{apply:O((l,s,c)=>{var u;const[p,m,f]=c;if(r.has(p))return o.apply(s,[p,m,f]);const{id:g,styleId:h}=Z((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(g&&g!==-1||h&&h!==-1)&&e({id:g,styleId:h,set:{property:p,value:m,priority:f},index:Re(s.parentRule)}),l.apply(s,c)})});const a=i.CSSStyleDeclaration.prototype.removeProperty;return i.CSSStyleDeclaration.prototype.removeProperty=new Proxy(a,{apply:O((l,s,c)=>{var u;const[p]=c;if(r.has(p))return a.apply(s,[p]);const{id:m,styleId:f}=Z((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(m&&m!==-1||f&&f!==-1)&&e({id:m,styleId:f,remove:{property:p},index:Re(s.parentRule)}),l.apply(s,c)})}),O(()=>{i.CSSStyleDeclaration.prototype.setProperty=o,i.CSSStyleDeclaration.prototype.removeProperty=a})}function Qr({mediaInteractionCb:e,blockClass:t,blockSelector:r,mirror:n,sampling:i,doc:o}){const a=O(s=>ve(O(c=>{const u=we(c);if(!u||U(u,t,r,!0))return;const{currentTime:p,volume:m,muted:f,playbackRate:g,loop:h}=u;e({type:s,id:n.getId(u),currentTime:p,volume:m,muted:f,playbackRate:g,loop:h})}),i.media||500)),l=[W("play",a(0),o),W("pause",a(1),o),W("seeked",a(2),o),W("volumechange",a(3),o),W("ratechange",a(4),o)];return O(()=>{l.forEach(s=>s())})}function Zr({fontCb:e,doc:t}){const r=t.defaultView;if(!r)return()=>{};const n=[],i=new WeakMap,o=r.FontFace;r.FontFace=function(s,c,u){const p=new o(s,c,u);return i.set(p,{family:s,buffer:typeof c!="string",descriptors:u,fontSource:typeof c=="string"?c:JSON.stringify(Array.from(new Uint8Array(c)))}),p};const a=he(t.fonts,"add",function(l){return function(s){return setTimeout(O(()=>{const c=i.get(s);c&&(e(c),i.delete(s))}),0),l.apply(this,[s])}});return n.push(()=>{r.FontFace=o}),n.push(a),O(()=>{n.forEach(l=>l())})}function en(e){const{doc:t,mirror:r,blockClass:n,blockSelector:i,selectionCb:o}=e;let a=!0;const l=O(()=>{const s=t.getSelection();if(!s||a&&s?.isCollapsed)return;a=s.isCollapsed||!1;const c=[],u=s.rangeCount||0;for(let p=0;p{}:he(r.customElements,"define",function(i){return function(o,a,l){try{t({define:{name:o}})}catch{console.warn(`Custom element callback failed for ${o}`)}return i.apply(this,[o,a,l])}})}function rn(e,t){const{mutationCb:r,mousemoveCb:n,mouseInteractionCb:i,scrollCb:o,viewportResizeCb:a,inputCb:l,mediaInteractionCb:s,styleSheetRuleCb:c,styleDeclarationCb:u,canvasMutationCb:p,fontCb:m,selectionCb:f,customElementCb:g}=e;e.mutationCb=(...h)=>{t.mutation&&t.mutation(...h),r(...h)},e.mousemoveCb=(...h)=>{t.mousemove&&t.mousemove(...h),n(...h)},e.mouseInteractionCb=(...h)=>{t.mouseInteraction&&t.mouseInteraction(...h),i(...h)},e.scrollCb=(...h)=>{t.scroll&&t.scroll(...h),o(...h)},e.viewportResizeCb=(...h)=>{t.viewportResize&&t.viewportResize(...h),a(...h)},e.inputCb=(...h)=>{t.input&&t.input(...h),l(...h)},e.mediaInteractionCb=(...h)=>{t.mediaInteaction&&t.mediaInteaction(...h),s(...h)},e.styleSheetRuleCb=(...h)=>{t.styleSheetRule&&t.styleSheetRule(...h),c(...h)},e.styleDeclarationCb=(...h)=>{t.styleDeclaration&&t.styleDeclaration(...h),u(...h)},e.canvasMutationCb=(...h)=>{t.canvasMutation&&t.canvasMutation(...h),p(...h)},e.fontCb=(...h)=>{t.font&&t.font(...h),m(...h)},e.selectionCb=(...h)=>{t.selection&&t.selection(...h),f(...h)},e.customElementCb=(...h)=>{t.customElement&&t.customElement(...h),g(...h)}}function nn(e,t={}){const r=e.doc.defaultView;if(!r)return()=>{};rn(e,t);let n;e.recordDOM&&(n=Ft(e,e.doc));const i=jr(e),o=Vr(e),a=Bt(e),l=qr(e,{win:r}),s=Xr(e),c=Qr(e);let u=()=>{},p=()=>{},m=()=>{},f=()=>{};e.recordDOM&&(u=Kr(e,{win:r}),p=Ut(e,e.doc),m=Yr(e,{win:r}),e.collectFonts&&(f=Zr(e)));const g=en(e),h=tn(e),y=[];for(const w of e.plugins)y.push(w.observer(w.callback,r,w.options));return O(()=>{re.forEach(w=>w.reset()),n?.disconnect(),i(),o(),a(),l(),s(),c(),u(),p(),m(),f(),g(),h(),y.forEach(w=>w())})}function Ne(e){return typeof window[e]<"u"}function De(e){return!!(typeof window[e]<"u"&&window[e].prototype&&"insertRule"in window[e].prototype&&"deleteRule"in window[e].prototype)}class Ht{constructor(t){this.generateIdFn=t,this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap}getId(t,r,n,i){const o=n||this.getIdToRemoteIdMap(t),a=i||this.getRemoteIdToIdMap(t);let l=o.get(r);return l||(l=this.generateIdFn(),o.set(r,l),a.set(l,r)),l}getIds(t,r){const n=this.getIdToRemoteIdMap(t),i=this.getRemoteIdToIdMap(t);return r.map(o=>this.getId(t,o,n,i))}getRemoteId(t,r,n){const i=n||this.getRemoteIdToIdMap(t);if(typeof r!="number")return r;const o=i.get(r);return o||-1}getRemoteIds(t,r){const n=this.getRemoteIdToIdMap(t);return r.map(i=>this.getRemoteId(t,i,n))}reset(t){if(!t){this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap;return}this.iframeIdToRemoteIdMap.delete(t),this.iframeRemoteIdToIdMap.delete(t)}getIdToRemoteIdMap(t){let r=this.iframeIdToRemoteIdMap.get(t);return r||(r=new Map,this.iframeIdToRemoteIdMap.set(t,r)),r}getRemoteIdToIdMap(t){let r=this.iframeRemoteIdToIdMap.get(t);return r||(r=new Map,this.iframeRemoteIdToIdMap.set(t,r)),r}}class on{constructor(t){this.iframes=new WeakMap,this.crossOriginIframeMap=new WeakMap,this.crossOriginIframeMirror=new Ht(gt),this.crossOriginIframeRootIdMap=new WeakMap,this.mutationCb=t.mutationCb,this.wrappedEmit=t.wrappedEmit,this.stylesheetManager=t.stylesheetManager,this.recordCrossOriginIframes=t.recordCrossOriginIframes,this.crossOriginIframeStyleMirror=new Ht(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)),this.mirror=t.mirror,this.recordCrossOriginIframes&&window.addEventListener("message",this.handleMessage.bind(this))}addIframe(t){this.iframes.set(t,!0),t.contentWindow&&this.crossOriginIframeMap.set(t.contentWindow,t)}addLoadListener(t){this.loadListener=t}attachIframe(t,r){var n;this.mutationCb({adds:[{parentId:this.mirror.getId(t),nextId:null,node:r}],removes:[],texts:[],attributes:[],isAttachIframe:!0}),(n=this.loadListener)===null||n===void 0||n.call(this,t),t.contentDocument&&t.contentDocument.adoptedStyleSheets&&t.contentDocument.adoptedStyleSheets.length>0&&this.stylesheetManager.adoptStyleSheets(t.contentDocument.adoptedStyleSheets,this.mirror.getId(t.contentDocument))}handleMessage(t){const r=t;if(r.data.type!=="rrweb"||r.origin!==r.data.origin||!t.source)return;const i=this.crossOriginIframeMap.get(t.source);if(!i)return;const o=this.transformCrossOriginEvent(i,r.data.event);o&&this.wrappedEmit(o,r.data.isCheckout)}transformCrossOriginEvent(t,r){var n;switch(r.type){case _.FullSnapshot:{this.crossOriginIframeMirror.reset(t),this.crossOriginIframeStyleMirror.reset(t),this.replaceIdOnNode(r.data.node,t);const i=r.data.node.id;return this.crossOriginIframeRootIdMap.set(t,i),this.patchRootIdOnNode(r.data.node,i),{timestamp:r.timestamp,type:_.IncrementalSnapshot,data:{source:C.Mutation,adds:[{parentId:this.mirror.getId(t),nextId:null,node:r.data.node}],removes:[],texts:[],attributes:[],isAttachIframe:!0}}}case _.Meta:case _.Load:case _.DomContentLoaded:return!1;case _.Plugin:return r;case _.Custom:return this.replaceIds(r.data.payload,t,["id","parentId","previousId","nextId"]),r;case _.IncrementalSnapshot:switch(r.data.source){case C.Mutation:return r.data.adds.forEach(i=>{this.replaceIds(i,t,["parentId","nextId","previousId"]),this.replaceIdOnNode(i.node,t);const o=this.crossOriginIframeRootIdMap.get(t);o&&this.patchRootIdOnNode(i.node,o)}),r.data.removes.forEach(i=>{this.replaceIds(i,t,["parentId","id"])}),r.data.attributes.forEach(i=>{this.replaceIds(i,t,["id"])}),r.data.texts.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.Drag:case C.TouchMove:case C.MouseMove:return r.data.positions.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.ViewportResize:return!1;case C.MediaInteraction:case C.MouseInteraction:case C.Scroll:case C.CanvasMutation:case C.Input:return this.replaceIds(r.data,t,["id"]),r;case C.StyleSheetRule:case C.StyleDeclaration:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleId"]),r;case C.Font:return r;case C.Selection:return r.data.ranges.forEach(i=>{this.replaceIds(i,t,["start","end"])}),r;case C.AdoptedStyleSheet:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleIds"]),(n=r.data.styles)===null||n===void 0||n.forEach(i=>{this.replaceStyleIds(i,t,["styleId"])}),r}}return!1}replace(t,r,n,i){for(const o of i)!Array.isArray(r[o])&&typeof r[o]!="number"||(Array.isArray(r[o])?r[o]=t.getIds(n,r[o]):r[o]=t.getId(n,r[o]));return r}replaceIds(t,r,n){return this.replace(this.crossOriginIframeMirror,t,r,n)}replaceStyleIds(t,r,n){return this.replace(this.crossOriginIframeStyleMirror,t,r,n)}replaceIdOnNode(t,r){this.replaceIds(t,r,["id","rootId"]),"childNodes"in t&&t.childNodes.forEach(n=>{this.replaceIdOnNode(n,r)})}patchRootIdOnNode(t,r){t.type!==A.Document&&!t.rootId&&(t.rootId=r),"childNodes"in t&&t.childNodes.forEach(n=>{this.patchRootIdOnNode(n,r)})}}class sn{constructor(t){this.shadowDoms=new WeakSet,this.restoreHandlers=[],this.mutationCb=t.mutationCb,this.scrollCb=t.scrollCb,this.bypassOptions=t.bypassOptions,this.mirror=t.mirror,this.init()}init(){this.reset(),this.patchAttachShadow(Element,document)}addShadowRoot(t,r){if(!ye(t)||this.shadowDoms.has(t))return;this.shadowDoms.add(t);const n=Ft(Object.assign(Object.assign({},this.bypassOptions),{doc:r,mutationCb:this.mutationCb,mirror:this.mirror,shadowDomManager:this}),t);this.restoreHandlers.push(()=>n.disconnect()),this.restoreHandlers.push(Bt(Object.assign(Object.assign({},this.bypassOptions),{scrollCb:this.scrollCb,doc:t,mirror:this.mirror}))),setTimeout(()=>{t.adoptedStyleSheets&&t.adoptedStyleSheets.length>0&&this.bypassOptions.stylesheetManager.adoptStyleSheets(t.adoptedStyleSheets,this.mirror.getId(t.host)),this.restoreHandlers.push(Ut({mirror:this.mirror,stylesheetManager:this.bypassOptions.stylesheetManager},t))},0)}observeAttachShadow(t){!t.contentWindow||!t.contentDocument||this.patchAttachShadow(t.contentWindow.Element,t.contentDocument)}patchAttachShadow(t,r){const n=this;this.restoreHandlers.push(he(t.prototype,"attachShadow",function(i){return function(o){const a=i.call(this,o);return this.shadowRoot&&Tt(this)&&n.addShadowRoot(this.shadowRoot,r),a}}))}reset(){this.restoreHandlers.forEach(t=>{try{t()}catch{}}),this.restoreHandlers=[],this.shadowDoms=new WeakSet}}/*! ***************************************************************************** +or you can use record.mirror to access the mirror instance during recording.`;let kt={map:{},getId(){return console.error(me),-1},getNode(){return console.error(me),null},removeNodeFromMap(){console.error(me)},has(){return console.error(me),!1},reset(){console.error(me)}};typeof window<"u"&&window.Proxy&&window.Reflect&&(kt=new Proxy(kt,{get(e,t,r){return t==="map"&&console.error(me),Reflect.get(e,t,r)}}));function Me(e,t,r={}){let n=null,i=0;return function(...o){const a=Date.now();!i&&r.leading===!1&&(i=a);const l=t-(a-i),s=this;l<=0||l>t?(n&&(clearTimeout(n),n=null),i=a,e.apply(s,o)):!n&&r.trailing!==!1&&(n=setTimeout(()=>{i=r.leading===!1?0:Date.now(),n=null,e.apply(s,o)},l))}}function Ae(e,t,r,n,i=window){const o=i.Object.getOwnPropertyDescriptor(e,t);return i.Object.defineProperty(e,t,n?r:{set(a){setTimeout(()=>{r.set.call(this,a)},0),o&&o.set&&o.set.call(this,a)}}),()=>Ae(e,t,o||{},!0)}function ge(e,t,r){try{if(!(t in e))return()=>{};const n=e[t],i=r(n);return typeof i=="function"&&(i.prototype=i.prototype||{},Object.defineProperties(i,{__rrweb_original__:{enumerable:!1,value:n}})),e[t]=i,()=>{e[t]=n}}catch{return()=>{}}}let Le=Date.now;/[1-9][0-9]{12}/.test(Date.now().toString())||(Le=()=>new Date().getTime());function xt(e){var t,r,n,i,o,a;const l=e.document;return{left:l.scrollingElement?l.scrollingElement.scrollLeft:e.pageXOffset!==void 0?e.pageXOffset:l?.documentElement.scrollLeft||((r=(t=l?.body)===null||t===void 0?void 0:t.parentElement)===null||r===void 0?void 0:r.scrollLeft)||((n=l?.body)===null||n===void 0?void 0:n.scrollLeft)||0,top:l.scrollingElement?l.scrollingElement.scrollTop:e.pageYOffset!==void 0?e.pageYOffset:l?.documentElement.scrollTop||((o=(i=l?.body)===null||i===void 0?void 0:i.parentElement)===null||o===void 0?void 0:o.scrollTop)||((a=l?.body)===null||a===void 0?void 0:a.scrollTop)||0}}function Rt(){return window.innerHeight||document.documentElement&&document.documentElement.clientHeight||document.body&&document.body.clientHeight}function Tt(){return window.innerWidth||document.documentElement&&document.documentElement.clientWidth||document.body&&document.body.clientWidth}function Dt(e){return e?e.nodeType===e.ELEMENT_NODE?e:e.parentElement:null}function U(e,t,r,n){if(!e)return!1;const i=Dt(e);if(!i)return!1;try{if(typeof t=="string"){if(i.classList.contains(t)||n&&i.closest("."+t)!==null)return!0}else if(Ne(i,t,n))return!0}catch{}return!!(r&&(i.matches(r)||n&&i.closest(r)!==null))}function Jr(e,t){return t.getId(e)!==-1}function it(e,t){return t.getId(e)===Ie}function Nt(e,t){if(be(e))return!1;const r=t.getId(e);return t.has(r)?e.parentNode&&e.parentNode.nodeType===e.DOCUMENT_NODE?!1:e.parentNode?Nt(e.parentNode,t):!0:!0}function ot(e){return!!e.changedTouches}function Xr(e=window){"NodeList"in e&&!e.NodeList.prototype.forEach&&(e.NodeList.prototype.forEach=Array.prototype.forEach),"DOMTokenList"in e&&!e.DOMTokenList.prototype.forEach&&(e.DOMTokenList.prototype.forEach=Array.prototype.forEach),Node.prototype.contains||(Node.prototype.contains=(...t)=>{let r=t[0];if(!(0 in t))throw new TypeError("1 argument is required");do if(this===r)return!0;while(r=r&&r.parentNode);return!1})}function At(e,t){return!!(e.nodeName==="IFRAME"&&t.getMeta(e))}function Lt(e,t){return!!(e.nodeName==="LINK"&&e.nodeType===e.ELEMENT_NODE&&e.getAttribute&&e.getAttribute("rel")==="stylesheet"&&t.getMeta(e))}function st(e){return!!e?.shadowRoot}class Kr{constructor(){this.id=1,this.styleIDMap=new WeakMap,this.idStyleMap=new Map}getId(t){var r;return(r=this.styleIDMap.get(t))!==null&&r!==void 0?r:-1}has(t){return this.styleIDMap.has(t)}add(t,r){if(this.has(t))return this.getId(t);let n;return r===void 0?n=this.id++:n=r,this.styleIDMap.set(t,n),this.idStyleMap.set(n,t),n}getStyle(t){return this.idStyleMap.get(t)||null}reset(){this.styleIDMap=new WeakMap,this.idStyleMap=new Map,this.id=1}generateId(){return this.id++}}function Ft(e){var t,r;let n=null;return((r=(t=e.getRootNode)===null||t===void 0?void 0:t.call(e))===null||r===void 0?void 0:r.nodeType)===Node.DOCUMENT_FRAGMENT_NODE&&e.getRootNode().host&&(n=e.getRootNode().host),n}function Yr(e){let t=e,r;for(;r=Ft(t);)t=r;return t}function Qr(e){const t=e.ownerDocument;if(!t)return!1;const r=Yr(e);return t.contains(r)}function Pt(e){const t=e.ownerDocument;return t?t.contains(e)||Qr(e):!1}var O=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(O||{}),C=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(C||{}),$=(e=>(e[e.MouseUp=0]="MouseUp",e[e.MouseDown=1]="MouseDown",e[e.Click=2]="Click",e[e.ContextMenu=3]="ContextMenu",e[e.DblClick=4]="DblClick",e[e.Focus=5]="Focus",e[e.Blur=6]="Blur",e[e.TouchStart=7]="TouchStart",e[e.TouchMove_Departed=8]="TouchMove_Departed",e[e.TouchEnd=9]="TouchEnd",e[e.TouchCancel=10]="TouchCancel",e))($||{}),ee=(e=>(e[e.Mouse=0]="Mouse",e[e.Pen=1]="Pen",e[e.Touch=2]="Touch",e))(ee||{}),ye=(e=>(e[e["2D"]=0]="2D",e[e.WebGL=1]="WebGL",e[e.WebGL2=2]="WebGL2",e))(ye||{});function Bt(e){return"__ln"in e}class Zr{constructor(){this.length=0,this.head=null,this.tail=null}get(t){if(t>=this.length)throw new Error("Position outside of list range");let r=this.head;for(let n=0;n`${e}@${t}`;class en{constructor(){this.frozen=!1,this.locked=!1,this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.mapRemoves=[],this.movedMap={},this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.processMutations=t=>{t.forEach(this.processMutation),this.emit()},this.emit=()=>{if(this.frozen||this.locked)return;const t=[],r=new Set,n=new Zr,i=s=>{let c=s,u=Ie;for(;u===Ie;)c=c&&c.nextSibling,u=c&&this.mirror.getId(c);return u},o=s=>{if(!s.parentNode||!Pt(s)||s.parentNode.tagName==="TEXTAREA")return;const c=be(s.parentNode)?this.mirror.getId(Ft(s)):this.mirror.getId(s.parentNode),u=i(s);if(c===-1||u===-1)return n.addNode(s);const f=pe(s,{doc:this.doc,mirror:this.mirror,blockClass:this.blockClass,blockSelector:this.blockSelector,maskTextClass:this.maskTextClass,maskTextSelector:this.maskTextSelector,skipChild:!0,newlyAddedElement:!0,inlineStylesheet:this.inlineStylesheet,maskInputOptions:this.maskInputOptions,maskTextFn:this.maskTextFn,maskInputFn:this.maskInputFn,slimDOMOptions:this.slimDOMOptions,dataURLOptions:this.dataURLOptions,recordCanvas:this.recordCanvas,inlineImages:this.inlineImages,onSerialize:m=>{At(m,this.mirror)&&this.iframeManager.addIframe(m),Lt(m,this.mirror)&&this.stylesheetManager.trackLinkElement(m),st(s)&&this.shadowDomManager.addShadowRoot(s.shadowRoot,this.doc)},onIframeLoad:(m,h)=>{this.iframeManager.attachIframe(m,h),this.shadowDomManager.observeAttachShadow(m)},onStylesheetLoad:(m,h)=>{this.stylesheetManager.attachLinkElement(m,h)}});f&&(t.push({parentId:c,nextId:u,node:f}),r.add(f.id))};for(;this.mapRemoves.length;)this.mirror.removeNodeFromMap(this.mapRemoves.shift());for(const s of this.movedSet)Ut(this.removes,s,this.mirror)&&!this.movedSet.has(s.parentNode)||o(s);for(const s of this.addedSet)!Ht(this.droppedSet,s)&&!Ut(this.removes,s,this.mirror)||Ht(this.movedSet,s)?o(s):this.droppedSet.add(s);let a=null;for(;n.length;){let s=null;if(a){const c=this.mirror.getId(a.value.parentNode),u=i(a.value);c!==-1&&u!==-1&&(s=a)}if(!s){let c=n.tail;for(;c;){const u=c;if(c=c.previous,u){const f=this.mirror.getId(u.value.parentNode);if(i(u.value)===-1)continue;if(f!==-1){s=u;break}else{const h=u.value;if(h.parentNode&&h.parentNode.nodeType===Node.DOCUMENT_FRAGMENT_NODE){const g=h.parentNode.host;if(this.mirror.getId(g)!==-1){s=u;break}}}}}}if(!s){for(;n.head;)n.removeNode(n.head.value);break}a=s.previous,n.removeNode(s.value),o(s.value)}const l={texts:this.texts.map(s=>{const c=s.node;return c.parentNode&&c.parentNode.tagName==="TEXTAREA"&&this.genTextAreaValueMutation(c.parentNode),{id:this.mirror.getId(c),value:s.value}}).filter(s=>!r.has(s.id)).filter(s=>this.mirror.has(s.id)),attributes:this.attributes.map(s=>{const{attributes:c}=s;if(typeof c.style=="string"){const u=JSON.stringify(s.styleDiff),f=JSON.stringify(s._unchangedStyles);u.length!r.has(s.id)).filter(s=>this.mirror.has(s.id)),removes:this.removes,adds:t};!l.texts.length&&!l.attributes.length&&!l.removes.length&&!l.adds.length||(this.texts=[],this.attributes=[],this.attributeMap=new WeakMap,this.removes=[],this.addedSet=new Set,this.movedSet=new Set,this.droppedSet=new Set,this.movedMap={},this.mutationCb(l))},this.genTextAreaValueMutation=t=>{let r=this.attributeMap.get(t);r||(r={node:t,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(r),this.attributeMap.set(t,r)),r.attributes.value=Array.from(t.childNodes,n=>n.textContent||"").join("")},this.processMutation=t=>{if(!it(t.target,this.mirror))switch(t.type){case"characterData":{const r=t.target.textContent;!U(t.target,this.blockClass,this.blockSelector,!1)&&r!==t.oldValue&&this.texts.push({value:Et(t.target,this.maskTextClass,this.maskTextSelector,!0)&&r?this.maskTextFn?this.maskTextFn(r,Dt(t.target)):r.replace(/[\S]/g,"*"):r,node:t.target});break}case"attributes":{const r=t.target;let n=t.attributeName,i=t.target.getAttribute(n);if(n==="value"){const a=rt(r);i=tt({element:r,maskInputOptions:this.maskInputOptions,tagName:r.tagName,type:a,value:i,maskInputFn:this.maskInputFn})}if(U(t.target,this.blockClass,this.blockSelector,!1)||i===t.oldValue)return;let o=this.attributeMap.get(t.target);if(r.tagName==="IFRAME"&&n==="src"&&!this.keepIframeSrcFn(i))if(!r.contentDocument)n="rr_src";else return;if(o||(o={node:t.target,attributes:{},styleDiff:{},_unchangedStyles:{}},this.attributes.push(o),this.attributeMap.set(t.target,o)),n==="type"&&r.tagName==="INPUT"&&(t.oldValue||"").toLowerCase()==="password"&&r.setAttribute("data-rr-is-password","true"),!Ot(r.tagName,n)&&(o.attributes[n]=_t(this.doc,ie(r.tagName),ie(n),i),n==="style")){if(!this.unattachedDoc)try{this.unattachedDoc=document.implementation.createHTMLDocument()}catch{this.unattachedDoc=this.doc}const a=this.unattachedDoc.createElement("span");t.oldValue&&a.setAttribute("style",t.oldValue);for(const l of Array.from(r.style)){const s=r.style.getPropertyValue(l),c=r.style.getPropertyPriority(l);s!==a.style.getPropertyValue(l)||c!==a.style.getPropertyPriority(l)?c===""?o.styleDiff[l]=s:o.styleDiff[l]=[s,c]:o._unchangedStyles[l]=[s,c]}for(const l of Array.from(a.style))r.style.getPropertyValue(l)===""&&(o.styleDiff[l]=!1)}break}case"childList":{if(U(t.target,this.blockClass,this.blockSelector,!0))return;if(t.target.tagName==="TEXTAREA"){this.genTextAreaValueMutation(t.target);return}t.addedNodes.forEach(r=>this.genAdds(r,t.target)),t.removedNodes.forEach(r=>{const n=this.mirror.getId(r),i=be(t.target)?this.mirror.getId(t.target.host):this.mirror.getId(t.target);U(t.target,this.blockClass,this.blockSelector,!1)||it(r,this.mirror)||!Jr(r,this.mirror)||(this.addedSet.has(r)?(at(this.addedSet,r),this.droppedSet.add(r)):this.addedSet.has(t.target)&&n===-1||Nt(t.target,this.mirror)||(this.movedSet.has(r)&&this.movedMap[Wt(n,i)]?at(this.movedSet,r):this.removes.push({parentId:i,id:n,isShadow:be(t.target)&&we(t.target)?!0:void 0})),this.mapRemoves.push(r))});break}}},this.genAdds=(t,r)=>{if(!this.processedNodeManager.inOtherBuffer(t,this)&&!(this.addedSet.has(t)||this.movedSet.has(t))){if(this.mirror.hasNode(t)){if(it(t,this.mirror))return;this.movedSet.add(t);let n=null;r&&this.mirror.hasNode(r)&&(n=this.mirror.getId(r)),n&&n!==-1&&(this.movedMap[Wt(this.mirror.getId(t),n)]=!0)}else this.addedSet.add(t),this.droppedSet.delete(t);U(t,this.blockClass,this.blockSelector,!1)||(t.childNodes.forEach(n=>this.genAdds(n)),st(t)&&t.shadowRoot.childNodes.forEach(n=>{this.processedNodeManager.add(n,this),this.genAdds(n,t)}))}}}init(t){["mutationCb","blockClass","blockSelector","maskTextClass","maskTextSelector","inlineStylesheet","maskInputOptions","maskTextFn","maskInputFn","keepIframeSrcFn","recordCanvas","inlineImages","slimDOMOptions","dataURLOptions","doc","mirror","iframeManager","stylesheetManager","shadowDomManager","canvasManager","processedNodeManager"].forEach(r=>{this[r]=t[r]})}freeze(){this.frozen=!0,this.canvasManager.freeze()}unfreeze(){this.frozen=!1,this.canvasManager.unfreeze(),this.emit()}isFrozen(){return this.frozen}lock(){this.locked=!0,this.canvasManager.lock()}unlock(){this.locked=!1,this.canvasManager.unlock(),this.emit()}reset(){this.shadowDomManager.reset(),this.canvasManager.reset()}}function at(e,t){e.delete(t),t.childNodes.forEach(r=>at(e,r))}function Ut(e,t,r){return e.length===0?!1:zt(e,t,r)}function zt(e,t,r){const{parentNode:n}=t;if(!n)return!1;const i=r.getId(n);return e.some(o=>o.id===i)?!0:zt(e,n,r)}function Ht(e,t){return e.size===0?!1:$t(e,t)}function $t(e,t){const{parentNode:r}=t;return r?e.has(r)?!0:$t(e,r):!1}let Ce;function tn(e){Ce=e}function rn(){Ce=void 0}const _=e=>Ce?(...r)=>{try{return e(...r)}catch(n){if(Ce&&Ce(n)===!0)return;throw n}}:e,oe=[];function _e(e){try{if("composedPath"in e){const t=e.composedPath();if(t.length)return t[0]}else if("path"in e&&e.path.length)return e.path[0]}catch{}return e&&e.target}function qt(e,t){var r,n;const i=new en;oe.push(i),i.init(e);let o=window.MutationObserver||window.__rrMutationObserver;const a=(n=(r=window?.Zone)===null||r===void 0?void 0:r.__symbol__)===null||n===void 0?void 0:n.call(r,"MutationObserver");a&&window[a]&&(o=window[a]);const l=new o(_(i.processMutations.bind(i)));return l.observe(t,{attributes:!0,attributeOldValue:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0}),l}function nn({mousemoveCb:e,sampling:t,doc:r,mirror:n}){if(t.mousemove===!1)return()=>{};const i=typeof t.mousemove=="number"?t.mousemove:50,o=typeof t.mousemoveCallback=="number"?t.mousemoveCallback:500;let a=[],l;const s=Me(_(f=>{const m=Date.now()-l;e(a.map(h=>(h.timeOffset-=m,h)),f),a=[],l=null}),o),c=_(Me(_(f=>{const m=_e(f),{clientX:h,clientY:g}=ot(f)?f.changedTouches[0]:f;l||(l=Le()),a.push({x:h,y:g,id:n.getId(m),timeOffset:Le()-l}),s(typeof DragEvent<"u"&&f instanceof DragEvent?C.Drag:f instanceof MouseEvent?C.MouseMove:C.TouchMove)}),i,{trailing:!1})),u=[W("mousemove",c,r),W("touchmove",c,r),W("drag",c,r)];return _(()=>{u.forEach(f=>f())})}function on({mouseInteractionCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){if(o.mouseInteraction===!1)return()=>{};const a=o.mouseInteraction===!0||o.mouseInteraction===void 0?{}:o.mouseInteraction,l=[];let s=null;const c=u=>f=>{const m=_e(f);if(U(m,n,i,!0))return;let h=null,g=u;if("pointerType"in f){switch(f.pointerType){case"mouse":h=ee.Mouse;break;case"touch":h=ee.Touch;break;case"pen":h=ee.Pen;break}h===ee.Touch?$[u]===$.MouseDown?g="TouchStart":$[u]===$.MouseUp&&(g="TouchEnd"):ee.Pen}else ot(f)&&(h=ee.Touch);h!==null?(s=h,(g.startsWith("Touch")&&h===ee.Touch||g.startsWith("Mouse")&&h===ee.Mouse)&&(h=null)):$[u]===$.Click&&(h=s,s=null);const p=ot(f)?f.changedTouches[0]:f;if(!p)return;const y=r.getId(m),{clientX:w,clientY:S}=p;_(e)(Object.assign({type:$[g],id:y,x:w,y:S},h!==null&&{pointerType:h}))};return Object.keys($).filter(u=>Number.isNaN(Number(u))&&!u.endsWith("_Departed")&&a[u]!==!1).forEach(u=>{let f=ie(u);const m=c(u);if(window.PointerEvent)switch($[u]){case $.MouseDown:case $.MouseUp:f=f.replace("mouse","pointer");break;case $.TouchStart:case $.TouchEnd:return}l.push(W(f,m,t))}),_(()=>{l.forEach(u=>u())})}function jt({scrollCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,sampling:o}){const a=_(Me(_(l=>{const s=_e(l);if(!s||U(s,n,i,!0))return;const c=r.getId(s);if(s===t&&t.defaultView){const u=xt(t.defaultView);e({id:c,x:u.left,y:u.top})}else e({id:c,x:s.scrollLeft,y:s.scrollTop})}),o.scroll||100));return W("scroll",a,t)}function sn({viewportResizeCb:e},{win:t}){let r=-1,n=-1;const i=_(Me(_(()=>{const o=Rt(),a=Tt();(r!==o||n!==a)&&(e({width:Number(a),height:Number(o)}),r=o,n=a)}),200));return W("resize",i,t)}const an=["INPUT","TEXTAREA","SELECT"],Gt=new WeakMap;function ln({inputCb:e,doc:t,mirror:r,blockClass:n,blockSelector:i,ignoreClass:o,ignoreSelector:a,maskInputOptions:l,maskInputFn:s,sampling:c,userTriggeredOnInput:u}){function f(S){let v=_e(S);const b=S.isTrusted,I=v&&v.tagName;if(v&&I==="OPTION"&&(v=v.parentElement),!v||!I||an.indexOf(I)<0||U(v,n,i,!0)||v.classList.contains(o)||a&&v.matches(a))return;let B=v.value,F=!1;const x=rt(v)||"";x==="radio"||x==="checkbox"?F=v.checked:(l[I.toLowerCase()]||l[x])&&(B=tt({element:v,maskInputOptions:l,tagName:I,type:x,value:B,maskInputFn:s})),m(v,u?{text:B,isChecked:F,userTriggered:b}:{text:B,isChecked:F});const R=v.name;x==="radio"&&R&&F&&t.querySelectorAll(`input[type="radio"][name="${R}"]`).forEach(j=>{if(j!==v){const G=j.value;m(j,u?{text:G,isChecked:!F,userTriggered:!1}:{text:G,isChecked:!F})}})}function m(S,v){const b=Gt.get(S);if(!b||b.text!==v.text||b.isChecked!==v.isChecked){Gt.set(S,v);const I=r.getId(S);_(e)(Object.assign(Object.assign({},v),{id:I}))}}const g=(c.input==="last"?["change"]:["input","change"]).map(S=>W(S,_(f),t)),p=t.defaultView;if(!p)return()=>{g.forEach(S=>S())};const y=p.Object.getOwnPropertyDescriptor(p.HTMLInputElement.prototype,"value"),w=[[p.HTMLInputElement.prototype,"value"],[p.HTMLInputElement.prototype,"checked"],[p.HTMLSelectElement.prototype,"value"],[p.HTMLTextAreaElement.prototype,"value"],[p.HTMLSelectElement.prototype,"selectedIndex"],[p.HTMLOptionElement.prototype,"selected"]];return y&&y.set&&g.push(...w.map(S=>Ae(S[0],S[1],{set(){_(f)({target:this,isTrusted:!1})}},!1,p))),_(()=>{g.forEach(S=>S())})}function Fe(e){const t=[];function r(n,i){if(Pe("CSSGroupingRule")&&n.parentRule instanceof CSSGroupingRule||Pe("CSSMediaRule")&&n.parentRule instanceof CSSMediaRule||Pe("CSSSupportsRule")&&n.parentRule instanceof CSSSupportsRule||Pe("CSSConditionRule")&&n.parentRule instanceof CSSConditionRule){const a=Array.from(n.parentRule.cssRules).indexOf(n);i.unshift(a)}else if(n.parentStyleSheet){const a=Array.from(n.parentStyleSheet.cssRules).indexOf(n);i.unshift(a)}return i}return r(e,t)}function te(e,t,r){let n,i;return e?(e.ownerNode?n=t.getId(e.ownerNode):i=r.getId(e),{styleId:i,id:n}):{}}function un({styleSheetRuleCb:e,mirror:t,stylesheetManager:r},{win:n}){if(!n.CSSStyleSheet||!n.CSSStyleSheet.prototype)return()=>{};const i=n.CSSStyleSheet.prototype.insertRule;n.CSSStyleSheet.prototype.insertRule=new Proxy(i,{apply:_((u,f,m)=>{const[h,g]=m,{id:p,styleId:y}=te(f,t,r.styleMirror);return(p&&p!==-1||y&&y!==-1)&&e({id:p,styleId:y,adds:[{rule:h,index:g}]}),u.apply(f,m)})});const o=n.CSSStyleSheet.prototype.deleteRule;n.CSSStyleSheet.prototype.deleteRule=new Proxy(o,{apply:_((u,f,m)=>{const[h]=m,{id:g,styleId:p}=te(f,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,removes:[{index:h}]}),u.apply(f,m)})});let a;n.CSSStyleSheet.prototype.replace&&(a=n.CSSStyleSheet.prototype.replace,n.CSSStyleSheet.prototype.replace=new Proxy(a,{apply:_((u,f,m)=>{const[h]=m,{id:g,styleId:p}=te(f,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,replace:h}),u.apply(f,m)})}));let l;n.CSSStyleSheet.prototype.replaceSync&&(l=n.CSSStyleSheet.prototype.replaceSync,n.CSSStyleSheet.prototype.replaceSync=new Proxy(l,{apply:_((u,f,m)=>{const[h]=m,{id:g,styleId:p}=te(f,t,r.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,replaceSync:h}),u.apply(f,m)})}));const s={};Be("CSSGroupingRule")?s.CSSGroupingRule=n.CSSGroupingRule:(Be("CSSMediaRule")&&(s.CSSMediaRule=n.CSSMediaRule),Be("CSSConditionRule")&&(s.CSSConditionRule=n.CSSConditionRule),Be("CSSSupportsRule")&&(s.CSSSupportsRule=n.CSSSupportsRule));const c={};return Object.entries(s).forEach(([u,f])=>{c[u]={insertRule:f.prototype.insertRule,deleteRule:f.prototype.deleteRule},f.prototype.insertRule=new Proxy(c[u].insertRule,{apply:_((m,h,g)=>{const[p,y]=g,{id:w,styleId:S}=te(h.parentStyleSheet,t,r.styleMirror);return(w&&w!==-1||S&&S!==-1)&&e({id:w,styleId:S,adds:[{rule:p,index:[...Fe(h),y||0]}]}),m.apply(h,g)})}),f.prototype.deleteRule=new Proxy(c[u].deleteRule,{apply:_((m,h,g)=>{const[p]=g,{id:y,styleId:w}=te(h.parentStyleSheet,t,r.styleMirror);return(y&&y!==-1||w&&w!==-1)&&e({id:y,styleId:w,removes:[{index:[...Fe(h),p]}]}),m.apply(h,g)})})}),_(()=>{n.CSSStyleSheet.prototype.insertRule=i,n.CSSStyleSheet.prototype.deleteRule=o,a&&(n.CSSStyleSheet.prototype.replace=a),l&&(n.CSSStyleSheet.prototype.replaceSync=l),Object.entries(s).forEach(([u,f])=>{f.prototype.insertRule=c[u].insertRule,f.prototype.deleteRule=c[u].deleteRule})})}function Vt({mirror:e,stylesheetManager:t},r){var n,i,o;let a=null;r.nodeName==="#document"?a=e.getId(r):a=e.getId(r.host);const l=r.nodeName==="#document"?(n=r.defaultView)===null||n===void 0?void 0:n.Document:(o=(i=r.ownerDocument)===null||i===void 0?void 0:i.defaultView)===null||o===void 0?void 0:o.ShadowRoot,s=l?.prototype?Object.getOwnPropertyDescriptor(l?.prototype,"adoptedStyleSheets"):void 0;return a===null||a===-1||!l||!s?()=>{}:(Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get(){var c;return(c=s.get)===null||c===void 0?void 0:c.call(this)},set(c){var u;const f=(u=s.set)===null||u===void 0?void 0:u.call(this,c);if(a!==null&&a!==-1)try{t.adoptStyleSheets(c,a)}catch{}return f}}),_(()=>{Object.defineProperty(r,"adoptedStyleSheets",{configurable:s.configurable,enumerable:s.enumerable,get:s.get,set:s.set})}))}function cn({styleDeclarationCb:e,mirror:t,ignoreCSSAttributes:r,stylesheetManager:n},{win:i}){const o=i.CSSStyleDeclaration.prototype.setProperty;i.CSSStyleDeclaration.prototype.setProperty=new Proxy(o,{apply:_((l,s,c)=>{var u;const[f,m,h]=c;if(r.has(f))return o.apply(s,[f,m,h]);const{id:g,styleId:p}=te((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(g&&g!==-1||p&&p!==-1)&&e({id:g,styleId:p,set:{property:f,value:m,priority:h},index:Fe(s.parentRule)}),l.apply(s,c)})});const a=i.CSSStyleDeclaration.prototype.removeProperty;return i.CSSStyleDeclaration.prototype.removeProperty=new Proxy(a,{apply:_((l,s,c)=>{var u;const[f]=c;if(r.has(f))return a.apply(s,[f]);const{id:m,styleId:h}=te((u=s.parentRule)===null||u===void 0?void 0:u.parentStyleSheet,t,n.styleMirror);return(m&&m!==-1||h&&h!==-1)&&e({id:m,styleId:h,remove:{property:f},index:Fe(s.parentRule)}),l.apply(s,c)})}),_(()=>{i.CSSStyleDeclaration.prototype.setProperty=o,i.CSSStyleDeclaration.prototype.removeProperty=a})}function dn({mediaInteractionCb:e,blockClass:t,blockSelector:r,mirror:n,sampling:i,doc:o}){const a=_(s=>Me(_(c=>{const u=_e(c);if(!u||U(u,t,r,!0))return;const{currentTime:f,volume:m,muted:h,playbackRate:g,loop:p}=u;e({type:s,id:n.getId(u),currentTime:f,volume:m,muted:h,playbackRate:g,loop:p})}),i.media||500)),l=[W("play",a(0),o),W("pause",a(1),o),W("seeked",a(2),o),W("volumechange",a(3),o),W("ratechange",a(4),o)];return _(()=>{l.forEach(s=>s())})}function fn({fontCb:e,doc:t}){const r=t.defaultView;if(!r)return()=>{};const n=[],i=new WeakMap,o=r.FontFace;r.FontFace=function(s,c,u){const f=new o(s,c,u);return i.set(f,{family:s,buffer:typeof c!="string",descriptors:u,fontSource:typeof c=="string"?c:JSON.stringify(Array.from(new Uint8Array(c)))}),f};const a=ge(t.fonts,"add",function(l){return function(s){return setTimeout(_(()=>{const c=i.get(s);c&&(e(c),i.delete(s))}),0),l.apply(this,[s])}});return n.push(()=>{r.FontFace=o}),n.push(a),_(()=>{n.forEach(l=>l())})}function hn(e){const{doc:t,mirror:r,blockClass:n,blockSelector:i,selectionCb:o}=e;let a=!0;const l=_(()=>{const s=t.getSelection();if(!s||a&&s?.isCollapsed)return;a=s.isCollapsed||!1;const c=[],u=s.rangeCount||0;for(let f=0;f{}:ge(r.customElements,"define",function(i){return function(o,a,l){try{t({define:{name:o}})}catch{console.warn(`Custom element callback failed for ${o}`)}return i.apply(this,[o,a,l])}})}function mn(e,t){const{mutationCb:r,mousemoveCb:n,mouseInteractionCb:i,scrollCb:o,viewportResizeCb:a,inputCb:l,mediaInteractionCb:s,styleSheetRuleCb:c,styleDeclarationCb:u,canvasMutationCb:f,fontCb:m,selectionCb:h,customElementCb:g}=e;e.mutationCb=(...p)=>{t.mutation&&t.mutation(...p),r(...p)},e.mousemoveCb=(...p)=>{t.mousemove&&t.mousemove(...p),n(...p)},e.mouseInteractionCb=(...p)=>{t.mouseInteraction&&t.mouseInteraction(...p),i(...p)},e.scrollCb=(...p)=>{t.scroll&&t.scroll(...p),o(...p)},e.viewportResizeCb=(...p)=>{t.viewportResize&&t.viewportResize(...p),a(...p)},e.inputCb=(...p)=>{t.input&&t.input(...p),l(...p)},e.mediaInteractionCb=(...p)=>{t.mediaInteaction&&t.mediaInteaction(...p),s(...p)},e.styleSheetRuleCb=(...p)=>{t.styleSheetRule&&t.styleSheetRule(...p),c(...p)},e.styleDeclarationCb=(...p)=>{t.styleDeclaration&&t.styleDeclaration(...p),u(...p)},e.canvasMutationCb=(...p)=>{t.canvasMutation&&t.canvasMutation(...p),f(...p)},e.fontCb=(...p)=>{t.font&&t.font(...p),m(...p)},e.selectionCb=(...p)=>{t.selection&&t.selection(...p),h(...p)},e.customElementCb=(...p)=>{t.customElement&&t.customElement(...p),g(...p)}}function gn(e,t={}){const r=e.doc.defaultView;if(!r)return()=>{};mn(e,t);let n;e.recordDOM&&(n=qt(e,e.doc));const i=nn(e),o=on(e),a=jt(e),l=sn(e,{win:r}),s=ln(e),c=dn(e);let u=()=>{},f=()=>{},m=()=>{},h=()=>{};e.recordDOM&&(u=un(e,{win:r}),f=Vt(e,e.doc),m=cn(e,{win:r}),e.collectFonts&&(h=fn(e)));const g=hn(e),p=pn(e),y=[];for(const w of e.plugins)y.push(w.observer(w.callback,r,w.options));return _(()=>{oe.forEach(w=>w.reset()),n?.disconnect(),i(),o(),a(),l(),s(),c(),u(),f(),m(),h(),g(),p(),y.forEach(w=>w())})}function Pe(e){return typeof window[e]<"u"}function Be(e){return!!(typeof window[e]<"u"&&window[e].prototype&&"insertRule"in window[e].prototype&&"deleteRule"in window[e].prototype)}class Jt{constructor(t){this.generateIdFn=t,this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap}getId(t,r,n,i){const o=n||this.getIdToRemoteIdMap(t),a=i||this.getRemoteIdToIdMap(t);let l=o.get(r);return l||(l=this.generateIdFn(),o.set(r,l),a.set(l,r)),l}getIds(t,r){const n=this.getIdToRemoteIdMap(t),i=this.getRemoteIdToIdMap(t);return r.map(o=>this.getId(t,o,n,i))}getRemoteId(t,r,n){const i=n||this.getRemoteIdToIdMap(t);if(typeof r!="number")return r;const o=i.get(r);return o||-1}getRemoteIds(t,r){const n=this.getRemoteIdToIdMap(t);return r.map(i=>this.getRemoteId(t,i,n))}reset(t){if(!t){this.iframeIdToRemoteIdMap=new WeakMap,this.iframeRemoteIdToIdMap=new WeakMap;return}this.iframeIdToRemoteIdMap.delete(t),this.iframeRemoteIdToIdMap.delete(t)}getIdToRemoteIdMap(t){let r=this.iframeIdToRemoteIdMap.get(t);return r||(r=new Map,this.iframeIdToRemoteIdMap.set(t,r)),r}getRemoteIdToIdMap(t){let r=this.iframeRemoteIdToIdMap.get(t);return r||(r=new Map,this.iframeRemoteIdToIdMap.set(t,r)),r}}class yn{constructor(t){this.iframes=new WeakMap,this.crossOriginIframeMap=new WeakMap,this.crossOriginIframeMirror=new Jt(Mt),this.crossOriginIframeRootIdMap=new WeakMap,this.mutationCb=t.mutationCb,this.wrappedEmit=t.wrappedEmit,this.stylesheetManager=t.stylesheetManager,this.recordCrossOriginIframes=t.recordCrossOriginIframes,this.crossOriginIframeStyleMirror=new Jt(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)),this.mirror=t.mirror,this.recordCrossOriginIframes&&window.addEventListener("message",this.handleMessage.bind(this))}addIframe(t){this.iframes.set(t,!0),t.contentWindow&&this.crossOriginIframeMap.set(t.contentWindow,t)}addLoadListener(t){this.loadListener=t}attachIframe(t,r){var n;this.mutationCb({adds:[{parentId:this.mirror.getId(t),nextId:null,node:r}],removes:[],texts:[],attributes:[],isAttachIframe:!0}),(n=this.loadListener)===null||n===void 0||n.call(this,t),t.contentDocument&&t.contentDocument.adoptedStyleSheets&&t.contentDocument.adoptedStyleSheets.length>0&&this.stylesheetManager.adoptStyleSheets(t.contentDocument.adoptedStyleSheets,this.mirror.getId(t.contentDocument))}handleMessage(t){const r=t;if(r.data.type!=="rrweb"||r.origin!==r.data.origin||!t.source)return;const i=this.crossOriginIframeMap.get(t.source);if(!i)return;const o=this.transformCrossOriginEvent(i,r.data.event);o&&this.wrappedEmit(o,r.data.isCheckout)}transformCrossOriginEvent(t,r){var n;switch(r.type){case O.FullSnapshot:{this.crossOriginIframeMirror.reset(t),this.crossOriginIframeStyleMirror.reset(t),this.replaceIdOnNode(r.data.node,t);const i=r.data.node.id;return this.crossOriginIframeRootIdMap.set(t,i),this.patchRootIdOnNode(r.data.node,i),{timestamp:r.timestamp,type:O.IncrementalSnapshot,data:{source:C.Mutation,adds:[{parentId:this.mirror.getId(t),nextId:null,node:r.data.node}],removes:[],texts:[],attributes:[],isAttachIframe:!0}}}case O.Meta:case O.Load:case O.DomContentLoaded:return!1;case O.Plugin:return r;case O.Custom:return this.replaceIds(r.data.payload,t,["id","parentId","previousId","nextId"]),r;case O.IncrementalSnapshot:switch(r.data.source){case C.Mutation:return r.data.adds.forEach(i=>{this.replaceIds(i,t,["parentId","nextId","previousId"]),this.replaceIdOnNode(i.node,t);const o=this.crossOriginIframeRootIdMap.get(t);o&&this.patchRootIdOnNode(i.node,o)}),r.data.removes.forEach(i=>{this.replaceIds(i,t,["parentId","id"])}),r.data.attributes.forEach(i=>{this.replaceIds(i,t,["id"])}),r.data.texts.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.Drag:case C.TouchMove:case C.MouseMove:return r.data.positions.forEach(i=>{this.replaceIds(i,t,["id"])}),r;case C.ViewportResize:return!1;case C.MediaInteraction:case C.MouseInteraction:case C.Scroll:case C.CanvasMutation:case C.Input:return this.replaceIds(r.data,t,["id"]),r;case C.StyleSheetRule:case C.StyleDeclaration:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleId"]),r;case C.Font:return r;case C.Selection:return r.data.ranges.forEach(i=>{this.replaceIds(i,t,["start","end"])}),r;case C.AdoptedStyleSheet:return this.replaceIds(r.data,t,["id"]),this.replaceStyleIds(r.data,t,["styleIds"]),(n=r.data.styles)===null||n===void 0||n.forEach(i=>{this.replaceStyleIds(i,t,["styleId"])}),r}}return!1}replace(t,r,n,i){for(const o of i)!Array.isArray(r[o])&&typeof r[o]!="number"||(Array.isArray(r[o])?r[o]=t.getIds(n,r[o]):r[o]=t.getId(n,r[o]));return r}replaceIds(t,r,n){return this.replace(this.crossOriginIframeMirror,t,r,n)}replaceStyleIds(t,r,n){return this.replace(this.crossOriginIframeStyleMirror,t,r,n)}replaceIdOnNode(t,r){this.replaceIds(t,r,["id","rootId"]),"childNodes"in t&&t.childNodes.forEach(n=>{this.replaceIdOnNode(n,r)})}patchRootIdOnNode(t,r){t.type!==A.Document&&!t.rootId&&(t.rootId=r),"childNodes"in t&&t.childNodes.forEach(n=>{this.patchRootIdOnNode(n,r)})}}class vn{constructor(t){this.shadowDoms=new WeakSet,this.restoreHandlers=[],this.mutationCb=t.mutationCb,this.scrollCb=t.scrollCb,this.bypassOptions=t.bypassOptions,this.mirror=t.mirror,this.init()}init(){this.reset(),this.patchAttachShadow(Element,document)}addShadowRoot(t,r){if(!we(t)||this.shadowDoms.has(t))return;this.shadowDoms.add(t);const n=qt(Object.assign(Object.assign({},this.bypassOptions),{doc:r,mutationCb:this.mutationCb,mirror:this.mirror,shadowDomManager:this}),t);this.restoreHandlers.push(()=>n.disconnect()),this.restoreHandlers.push(jt(Object.assign(Object.assign({},this.bypassOptions),{scrollCb:this.scrollCb,doc:t,mirror:this.mirror}))),setTimeout(()=>{t.adoptedStyleSheets&&t.adoptedStyleSheets.length>0&&this.bypassOptions.stylesheetManager.adoptStyleSheets(t.adoptedStyleSheets,this.mirror.getId(t.host)),this.restoreHandlers.push(Vt({mirror:this.mirror,stylesheetManager:this.bypassOptions.stylesheetManager},t))},0)}observeAttachShadow(t){!t.contentWindow||!t.contentDocument||this.patchAttachShadow(t.contentWindow.Element,t.contentDocument)}patchAttachShadow(t,r){const n=this;this.restoreHandlers.push(ge(t.prototype,"attachShadow",function(i){return function(o){const a=i.call(this,o);return this.shadowRoot&&Pt(this)&&n.addShadowRoot(this.shadowRoot,r),a}}))}reset(){this.restoreHandlers.forEach(t=>{try{t()}catch{}}),this.restoreHandlers=[],this.shadowDoms=new WeakSet}}/*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any @@ -13,10 +13,10 @@ or you can use record.mirror to access the mirror instance during recording.`;le LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */function an(e,t){var r={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var i=0,n=Object.getOwnPropertySymbols(e);i"u"?[]:new Uint8Array(256),Ae=0;Ae>2],i+=me[(t[r]&3)<<4|t[r+1]>>4],i+=me[(t[r+1]&15)<<2|t[r+2]>>6],i+=me[t[r+2]&63];return n%3===2?i=i.substring(0,i.length-1)+"=":n%3===1&&(i=i.substring(0,i.length-2)+"=="),i};const zt=new Map;function dn(e,t){let r=zt.get(e);return r||(r=new Map,zt.set(e,r)),r.has(t)||r.set(t,[]),r.get(t)}const $t=(e,t,r)=>{if(!e||!(jt(e,t)||typeof e=="object"))return;const n=e.constructor.name,i=dn(r,n);let o=i.indexOf(e);return o===-1&&(o=i.length,i.push(e)),o};function Le(e,t,r){if(e instanceof Array)return e.map(n=>Le(n,t,r));if(e===null)return e;if(e instanceof Float32Array||e instanceof Float64Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Uint8Array||e instanceof Uint16Array||e instanceof Int16Array||e instanceof Int8Array||e instanceof Uint8ClampedArray)return{rr_type:e.constructor.name,args:[Object.values(e)]};if(e instanceof ArrayBuffer){const n=e.constructor.name,i=un(e);return{rr_type:n,base64:i}}else{if(e instanceof DataView)return{rr_type:e.constructor.name,args:[Le(e.buffer,t,r),e.byteOffset,e.byteLength]};if(e instanceof HTMLImageElement){const n=e.constructor.name,{src:i}=e;return{rr_type:n,src:i}}else if(e instanceof HTMLCanvasElement){const n="HTMLImageElement",i=e.toDataURL();return{rr_type:n,src:i}}else{if(e instanceof ImageData)return{rr_type:e.constructor.name,args:[Le(e.data,t,r),e.width,e.height]};if(jt(e,t)||typeof e=="object"){const n=e.constructor.name,i=$t(e,t,r);return{rr_type:n,index:i}}}}return e}const Gt=(e,t,r)=>e.map(n=>Le(n,t,r)),jt=(e,t)=>!!["WebGLActiveInfo","WebGLBuffer","WebGLFramebuffer","WebGLProgram","WebGLRenderbuffer","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebGLVertexArrayObject","WebGLVertexArrayObjectOES"].filter(i=>typeof t[i]=="function").find(i=>e instanceof t[i]);function fn(e,t,r,n){const i=[],o=Object.getOwnPropertyNames(t.CanvasRenderingContext2D.prototype);for(const a of o)try{if(typeof t.CanvasRenderingContext2D.prototype[a]!="function")continue;const l=he(t.CanvasRenderingContext2D.prototype,a,function(s){return function(...c){return U(this.canvas,r,n,!0)||setTimeout(()=>{const u=Gt(c,t,this);e(this.canvas,{type:pe["2D"],property:a,args:u})},0),s.apply(this,c)}});i.push(l)}catch{const s=ke(t.CanvasRenderingContext2D.prototype,a,{set(c){e(this.canvas,{type:pe["2D"],property:a,args:[c],setter:!0})}});i.push(s)}return()=>{i.forEach(a=>a())}}function hn(e){return e==="experimental-webgl"?"webgl":e}function Vt(e,t,r,n){const i=[];try{const o=he(e.HTMLCanvasElement.prototype,"getContext",function(a){return function(l,...s){if(!U(this,t,r,!0)){const c=hn(l);if("__context"in this||(this.__context=c),n&&["webgl","webgl2"].includes(c))if(s[0]&&typeof s[0]=="object"){const u=s[0];u.preserveDrawingBuffer||(u.preserveDrawingBuffer=!0)}else s.splice(0,1,{preserveDrawingBuffer:!0})}return a.apply(this,[l,...s])}});i.push(o)}catch{console.error("failed to patch HTMLCanvasElement.prototype.getContext")}return()=>{i.forEach(o=>o())}}function qt(e,t,r,n,i,o,a){const l=[],s=Object.getOwnPropertyNames(e);for(const c of s)if(!["isContextLost","canvas","drawingBufferWidth","drawingBufferHeight"].includes(c))try{if(typeof e[c]!="function")continue;const u=he(e,c,function(p){return function(...m){const f=p.apply(this,m);if($t(f,a,this),"tagName"in this.canvas&&!U(this.canvas,n,i,!0)){const g=Gt(m,a,this),h={type:t,property:c,args:g};r(this.canvas,h)}return f}});l.push(u)}catch{const p=ke(e,c,{set(m){r(this.canvas,{type:t,property:c,args:[m],setter:!0})}});l.push(p)}return l}function pn(e,t,r,n,i){const o=[];return o.push(...qt(t.WebGLRenderingContext.prototype,pe.WebGL,e,r,n,i,t)),typeof t.WebGL2RenderingContext<"u"&&o.push(...qt(t.WebGL2RenderingContext.prototype,pe.WebGL2,e,r,n,i,t)),()=>{o.forEach(a=>a())}}function mn(e,t){var r=t===void 0?null:t,n=e.toString(),i=n.split(` + ***************************************************************************** */function Sn(e,t){var r={};for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var i=0,n=Object.getOwnPropertySymbols(e);i"u"?[]:new Uint8Array(256),We=0;We>2],i+=ve[(t[r]&3)<<4|t[r+1]>>4],i+=ve[(t[r+1]&15)<<2|t[r+2]>>6],i+=ve[t[r+2]&63];return n%3===2?i=i.substring(0,i.length-1)+"=":n%3===1&&(i=i.substring(0,i.length-2)+"=="),i};const Xt=new Map;function Mn(e,t){let r=Xt.get(e);return r||(r=new Map,Xt.set(e,r)),r.has(t)||r.set(t,[]),r.get(t)}const Kt=(e,t,r)=>{if(!e||!(Qt(e,t)||typeof e=="object"))return;const n=e.constructor.name,i=Mn(r,n);let o=i.indexOf(e);return o===-1&&(o=i.length,i.push(e)),o};function Ue(e,t,r){if(e instanceof Array)return e.map(n=>Ue(n,t,r));if(e===null)return e;if(e instanceof Float32Array||e instanceof Float64Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Uint8Array||e instanceof Uint16Array||e instanceof Int16Array||e instanceof Int8Array||e instanceof Uint8ClampedArray)return{rr_type:e.constructor.name,args:[Object.values(e)]};if(e instanceof ArrayBuffer){const n=e.constructor.name,i=In(e);return{rr_type:n,base64:i}}else{if(e instanceof DataView)return{rr_type:e.constructor.name,args:[Ue(e.buffer,t,r),e.byteOffset,e.byteLength]};if(e instanceof HTMLImageElement){const n=e.constructor.name,{src:i}=e;return{rr_type:n,src:i}}else if(e instanceof HTMLCanvasElement){const n="HTMLImageElement",i=e.toDataURL();return{rr_type:n,src:i}}else{if(e instanceof ImageData)return{rr_type:e.constructor.name,args:[Ue(e.data,t,r),e.width,e.height]};if(Qt(e,t)||typeof e=="object"){const n=e.constructor.name,i=Kt(e,t,r);return{rr_type:n,index:i}}}}return e}const Yt=(e,t,r)=>e.map(n=>Ue(n,t,r)),Qt=(e,t)=>!!["WebGLActiveInfo","WebGLBuffer","WebGLFramebuffer","WebGLProgram","WebGLRenderbuffer","WebGLShader","WebGLShaderPrecisionFormat","WebGLTexture","WebGLUniformLocation","WebGLVertexArrayObject","WebGLVertexArrayObjectOES"].filter(i=>typeof t[i]=="function").find(i=>e instanceof t[i]);function Cn(e,t,r,n){const i=[],o=Object.getOwnPropertyNames(t.CanvasRenderingContext2D.prototype);for(const a of o)try{if(typeof t.CanvasRenderingContext2D.prototype[a]!="function")continue;const l=ge(t.CanvasRenderingContext2D.prototype,a,function(s){return function(...c){return U(this.canvas,r,n,!0)||setTimeout(()=>{const u=Yt(c,t,this);e(this.canvas,{type:ye["2D"],property:a,args:u})},0),s.apply(this,c)}});i.push(l)}catch{const s=Ae(t.CanvasRenderingContext2D.prototype,a,{set(c){e(this.canvas,{type:ye["2D"],property:a,args:[c],setter:!0})}});i.push(s)}return()=>{i.forEach(a=>a())}}function _n(e){return e==="experimental-webgl"?"webgl":e}function Zt(e,t,r,n){const i=[];try{const o=ge(e.HTMLCanvasElement.prototype,"getContext",function(a){return function(l,...s){if(!U(this,t,r,!0)){const c=_n(l);if("__context"in this||(this.__context=c),n&&["webgl","webgl2"].includes(c))if(s[0]&&typeof s[0]=="object"){const u=s[0];u.preserveDrawingBuffer||(u.preserveDrawingBuffer=!0)}else s.splice(0,1,{preserveDrawingBuffer:!0})}return a.apply(this,[l,...s])}});i.push(o)}catch{console.error("failed to patch HTMLCanvasElement.prototype.getContext")}return()=>{i.forEach(o=>o())}}function er(e,t,r,n,i,o,a){const l=[],s=Object.getOwnPropertyNames(e);for(const c of s)if(!["isContextLost","canvas","drawingBufferWidth","drawingBufferHeight"].includes(c))try{if(typeof e[c]!="function")continue;const u=ge(e,c,function(f){return function(...m){const h=f.apply(this,m);if(Kt(h,a,this),"tagName"in this.canvas&&!U(this.canvas,n,i,!0)){const g=Yt(m,a,this),p={type:t,property:c,args:g};r(this.canvas,p)}return h}});l.push(u)}catch{const f=Ae(e,c,{set(m){r(this.canvas,{type:t,property:c,args:[m],setter:!0})}});l.push(f)}return l}function On(e,t,r,n,i){const o=[];return o.push(...er(t.WebGLRenderingContext.prototype,ye.WebGL,e,r,n,i,t)),typeof t.WebGL2RenderingContext<"u"&&o.push(...er(t.WebGL2RenderingContext.prototype,ye.WebGL2,e,r,n,i,t)),()=>{o.forEach(a=>a())}}function En(e,t){var r=t===void 0?null:t,n=e.toString(),i=n.split(` `);i.pop(),i.shift();for(var o=i[0].search(/\S/),a=/(['"])__worker_loader_strict__(['"])/g,l=0,s=i.length;l"u"?[]:new Uint8Array(256),n=0;n>2],f+=t[(u[p]&3)<<4|u[p+1]>>4],f+=t[(u[p+1]&15)<<2|u[p+2]>>6],f+=t[u[p+2]&63];return m%3===2?f=f.substring(0,f.length-1)+"=":m%3===1&&(f=f.substring(0,f.length-2)+"=="),f};const o=new Map,a=new Map;function l(c,u,p){return e(this,void 0,void 0,function*(){const m=`${c}-${u}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const f=new OffscreenCanvas(c,u);f.getContext("2d");const h=yield(yield f.convertToBlob(p)).arrayBuffer(),y=i(h);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(c){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:u,bitmap:p,width:m,height:f,dataURLOptions:g}=c.data,h=l(m,f,g),y=new OffscreenCanvas(m,f);y.getContext("2d").drawImage(p,0,0),p.close();const v=yield y.convertToBlob(g),S=v.type,b=yield v.arrayBuffer(),M=i(b);if(!o.has(u)&&(yield h)===M)return o.set(u,M),s.postMessage({id:u});if(o.get(u)===M)return s.postMessage({id:u});s.postMessage({id:u,type:S,base64:M,width:m,height:f}),o.set(u,M)}else return s.postMessage({id:c.data.id})})}})()},null);class vn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,c)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(c)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Vt(r,n,i,!0),l=new Map,s=new Sn;s.onmessage=g=>{const{id:h}=g.data;if(l.set(h,!1),!("base64"in g.data))return;const{base64:y,type:w,width:v,height:S}=g.data;this.mutationCb({id:h,type:pe["2D"],commands:[{property:"clearRect",args:[0,0,v,S]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const c=1e3/t;let u=0,p;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(h=>{U(h,n,i,!0)||g.push(h)}),g},f=g=>{if(u&&g-uln(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(h);if(l.get(w)||h.width===0||h.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(h.__context)){const S=h.getContext(h.__context);((y=S?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&S.clear(S.COLOR_BUFFER_BIT)}const v=yield createImageBitmap(h);s.postMessage({id:w,bitmap:v,width:h.width,height:h.height,dataURLOptions:o.dataURLOptions},[v])})),p=requestAnimationFrame(f)};p=requestAnimationFrame(f),this.resetObservers=()=>{a(),cancelAnimationFrame(p)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Vt(t,r,n,!1),o=fn(this.processMutation.bind(this),t,r,n),a=pn(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>an(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class bn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Br,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:ft(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class wn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Te()})}let D,Pe,Ze,Fe=!1;const q=hr();function Me(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:c=null,inlineStylesheet:u=!0,maskAllInputs:p,maskInputOptions:m,slimDOMOptions:f,maskInputFn:g,maskTextFn:h,hooks:y,packFn:w,sampling:v={},dataURLOptions:S={},mousemoveWait:b,recordDOM:M=!0,recordCanvas:F=!1,recordCrossOriginIframes:P=!1,recordAfter:k=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:T=!1,collectFonts:j=!1,inlineImages:V=!1,plugins:x,keepIframeSrcFn:oe=()=>!1,ignoreCSSAttributes:H=new Set([]),errorHandler:ee}=e;$r(ee);const K=P?window.parent===window:!0;let $e=!1;if(!K)try{window.parent.document&&($e=!1)}catch{$e=!0}if(K&&!t)throw new Error("emit function is required");b!==void 0&&v.mousemove===void 0&&(v.mousemove=b),q.reset();const at=p===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},lt=f===!0||f==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:f==="all",headMetaDescKeywords:f==="all"}:f||{};Fr();let nr,ct=0;const ir=I=>{for(const X of x||[])X.eventProcessor&&(I=X.eventProcessor(I));return w&&!$e&&(I=w(I)),I};D=(I,X)=>{var N;if(!((N=re[0])===null||N===void 0)&&N.isFrozen()&&I.type!==_.FullSnapshot&&!(I.type===_.IncrementalSnapshot&&I.data.source===C.Mutation)&&re.forEach(z=>z.unfreeze()),K)t?.(ir(I),X);else if($e){const z={type:"rrweb",event:ir(I),origin:window.location.origin,isCheckout:X};window.parent.postMessage(z,"*")}if(I.type===_.FullSnapshot)nr=I,ct=0;else if(I.type===_.IncrementalSnapshot){if(I.data.source===C.Mutation&&I.data.isAttachIframe)return;ct++;const z=n&&ct>=n,le=r&&I.timestamp-nr.timestamp>r;(z||le)&&Pe(!0)}};const Ge=I=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Mutation},I)}))},or=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Scroll},I)})),sr=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},I)})),Un=I=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},I)})),se=new bn({mutationCb:Ge,adoptedStyleSheetCb:Un}),ae=new on({mirror:q,mutationCb:Ge,stylesheetManager:se,recordCrossOriginIframes:P,wrappedEmit:D});for(const I of x||[])I.getMirror&&I.getMirror({nodeMirror:q,crossOriginIframeMirror:ae.crossOriginIframeMirror,crossOriginIframeStyleMirror:ae.crossOriginIframeStyleMirror});const ut=new wn;Ze=new vn({recordCanvas:F,mutationCb:sr,win:window,blockClass:i,blockSelector:o,mirror:q,sampling:v.canvas,dataURLOptions:S});const je=new sn({mutationCb:Ge,scrollCb:or,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskInputOptions:at,dataURLOptions:S,maskTextFn:h,maskInputFn:g,recordCanvas:F,inlineImages:V,sampling:v,slimDOMOptions:lt,iframeManager:ae,stylesheetManager:se,canvasManager:Ze,keepIframeSrcFn:oe,processedNodeManager:ut},mirror:q});Pe=(I=!1)=>{if(!M)return;D(L({type:_.Meta,data:{href:window.location.href,width:Ct(),height:It()}}),I),se.reset(),je.init(),re.forEach(N=>N.lock());const X=Lr(document,{mirror:q,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskAllInputs:at,maskTextFn:h,slimDOM:lt,dataURLOptions:S,recordCanvas:F,inlineImages:V,onSerialize:N=>{xt(N,q)&&ae.addIframe(N),Et(N,q)&&se.trackLinkElement(N),Ye(N)&&je.addShadowRoot(N.shadowRoot,document)},onIframeLoad:(N,z)=>{ae.attachIframe(N,z),je.observeAttachShadow(N)},onStylesheetLoad:(N,z)=>{se.attachLinkElement(N,z)},keepIframeSrcFn:oe});if(!X)return console.warn("Failed to snapshot the document");D(L({type:_.FullSnapshot,data:{node:X,initialOffset:Mt(window)}}),I),re.forEach(N=>N.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&se.adoptStyleSheets(document.adoptedStyleSheets,q.getId(document))};try{const I=[],X=z=>{var le;return O(nn)({mutationCb:Ge,mousemoveCb:(R,dt)=>D(L({type:_.IncrementalSnapshot,data:{source:dt,positions:R}})),mouseInteractionCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},R)})),scrollCb:or,viewportResizeCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},R)})),inputCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Input},R)})),mediaInteractionCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},R)})),styleSheetRuleCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},R)})),styleDeclarationCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},R)})),canvasMutationCb:sr,fontCb:R=>D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Font},R)})),selectionCb:R=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.Selection},R)}))},customElementCb:R=>{D(L({type:_.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},R)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:c,maskInputOptions:at,inlineStylesheet:u,sampling:v,recordDOM:M,recordCanvas:F,inlineImages:V,userTriggeredOnInput:T,collectFonts:j,doc:z,maskInputFn:g,maskTextFn:h,keepIframeSrcFn:oe,blockSelector:o,slimDOMOptions:lt,dataURLOptions:S,mirror:q,iframeManager:ae,stylesheetManager:se,shadowDomManager:je,processedNodeManager:ut,canvasManager:Ze,ignoreCSSAttributes:H,plugins:((le=x?.filter(R=>R.observer))===null||le===void 0?void 0:le.map(R=>({observer:R.observer,options:R.options,callback:dt=>D(L({type:_.Plugin,data:{plugin:R.name,payload:dt}}))})))||[]},y)};ae.addLoadListener(z=>{try{I.push(X(z.contentDocument))}catch(le){console.warn(le)}});const N=()=>{Pe(),I.push(X(document)),Fe=!0};return document.readyState==="interactive"||document.readyState==="complete"?N():(I.push(W("DOMContentLoaded",()=>{D(L({type:_.DomContentLoaded,data:{}})),k==="DOMContentLoaded"&&N()})),I.push(W("load",()=>{D(L({type:_.Load,data:{}})),k==="load"&&N()},window))),()=>{I.forEach(z=>z()),ut.destroy(),Fe=!1,Gr()}}catch(I){console.warn(I)}}Me.addCustomEvent=(e,t)=>{if(!Fe)throw new Error("please add custom event after start recording");D(L({type:_.Custom,data:{tag:e,payload:t}}))},Me.freezePage=()=>{re.forEach(e=>e.freeze())},Me.takeFullSnapshot=e=>{if(!Fe)throw new Error("please take full snapshot after start recording");Pe(e)},Me.mirror=q;var Mn={DEBUG:!1,LIB_VERSION:"2.53.0"},B;if(typeof window>"u"){var Jt={hostname:""};B={navigator:{userAgent:""},document:{location:Jt,referrer:""},screen:{width:0,height:0},location:Jt}}else B=window;var Be=24*60*60*1e3,We=Array.prototype,In=Function.prototype,Xt=Object.prototype,ne=We.slice,Ie=Xt.toString,Ue=Xt.hasOwnProperty,Ce=B.console,Oe=B.navigator,G=B.document,He=B.opera,ze=B.screen,ie=Oe.userAgent,et=In.bind,Kt=We.forEach,Yt=We.indexOf,Qt=We.map,Cn=Array.isArray,tt={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},J={log:function(){},warn:function(){},error:function(){},critical:function(){if(!d.isUndefined(Ce)&&Ce){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{Ce.error.apply(Ce,e)}catch{d.each(e,function(r){Ce.error(r)})}}}},rt=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(J,arguments)}},On=function(e){return{log:rt(J.log,e),error:rt(J.error,e),critical:rt(J.critical,e)}};d.bind=function(e,t){var r,n;if(et&&e.bind===et)return et.apply(e,ne.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=ne.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(ne.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(ne.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(Kt&&e.forEach===Kt)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,c="",u="",p=0,m=a,f=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,f=[],Ie.apply(g)==="[object Array]"){for(p=g.length,s=0;s"u"?[]:new Uint8Array(256),n=0;n>2],h+=t[(u[f]&3)<<4|u[f+1]>>4],h+=t[(u[f+1]&15)<<2|u[f+2]>>6],h+=t[u[f+2]&63];return m%3===2?h=h.substring(0,h.length-1)+"=":m%3===1&&(h=h.substring(0,h.length-2)+"=="),h};const o=new Map,a=new Map;function l(c,u,f){return e(this,void 0,void 0,function*(){const m=`${c}-${u}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const h=new OffscreenCanvas(c,u);h.getContext("2d");const p=yield(yield h.convertToBlob(f)).arrayBuffer(),y=i(p);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(c){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:u,bitmap:f,width:m,height:h,dataURLOptions:g}=c.data,p=l(m,h,g),y=new OffscreenCanvas(m,h);y.getContext("2d").drawImage(f,0,0),f.close();const S=yield y.convertToBlob(g),v=S.type,b=yield S.arrayBuffer(),I=i(b);if(!o.has(u)&&(yield p)===I)return o.set(u,I),s.postMessage({id:u});if(o.get(u)===I)return s.postMessage({id:u});s.postMessage({id:u,type:v,base64:I,width:m,height:h}),o.set(u,I)}else return s.postMessage({id:c.data.id})})}})()},null);class Tn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,c)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(c)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Zt(r,n,i,!0),l=new Map,s=new Rn;s.onmessage=g=>{const{id:p}=g.data;if(l.set(p,!1),!("base64"in g.data))return;const{base64:y,type:w,width:S,height:v}=g.data;this.mutationCb({id:p,type:ye["2D"],commands:[{property:"clearRect",args:[0,0,S,v]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const c=1e3/t;let u=0,f;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(p=>{U(p,n,i,!0)||g.push(p)}),g},h=g=>{if(u&&g-ubn(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(p);if(l.get(w)||p.width===0||p.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(p.__context)){const v=p.getContext(p.__context);((y=v?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&v.clear(v.COLOR_BUFFER_BIT)}const S=yield createImageBitmap(p);s.postMessage({id:w,bitmap:S,width:p.width,height:p.height,dataURLOptions:o.dataURLOptions},[S])})),f=requestAnimationFrame(h)};f=requestAnimationFrame(h),this.resetObservers=()=>{a(),cancelAnimationFrame(f)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Zt(t,r,n,!1),o=Cn(this.processMutation.bind(this),t,r,n),a=On(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>Sn(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class Dn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Kr,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:St(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class Nn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Le()})}let N,ze,lt,He=!1;const V=_r();function Oe(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:c=null,inlineStylesheet:u=!0,maskAllInputs:f,maskInputOptions:m,slimDOMOptions:h,maskInputFn:g,maskTextFn:p,hooks:y,packFn:w,sampling:S={},dataURLOptions:v={},mousemoveWait:b,recordDOM:I=!0,recordCanvas:B=!1,recordCrossOriginIframes:F=!1,recordAfter:x=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:R=!1,collectFonts:j=!1,inlineImages:G=!1,plugins:E,keepIframeSrcFn:le=()=>!1,ignoreCSSAttributes:z=new Set([]),errorHandler:ne}=e;tn(ne);const Z=F?window.parent===window:!0;let Qe=!1;if(!Z)try{window.parent.document&&(Qe=!1)}catch{Qe=!0}if(Z&&!t)throw new Error("emit function is required");b!==void 0&&S.mousemove===void 0&&(S.mousemove=b),V.reset();const pt=f===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},mt=h===!0||h==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:h==="all",headMetaDescKeywords:h==="all"}:h||{};Xr();let mr,gt=0;const gr=M=>{for(const K of E||[])K.eventProcessor&&(M=K.eventProcessor(M));return w&&!Qe&&(M=w(M)),M};N=(M,K)=>{var D;if(!((D=oe[0])===null||D===void 0)&&D.isFrozen()&&M.type!==O.FullSnapshot&&!(M.type===O.IncrementalSnapshot&&M.data.source===C.Mutation)&&oe.forEach(H=>H.unfreeze()),Z)t?.(gr(M),K);else if(Qe){const H={type:"rrweb",event:gr(M),origin:window.location.origin,isCheckout:K};window.parent.postMessage(H,"*")}if(M.type===O.FullSnapshot)mr=M,gt=0;else if(M.type===O.IncrementalSnapshot){if(M.data.source===C.Mutation&&M.data.isAttachIframe)return;gt++;const H=n&>>=n,de=r&&M.timestamp-mr.timestamp>r;(H||de)&&ze(!0)}};const Ze=M=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Mutation},M)}))},yr=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Scroll},M)})),vr=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},M)})),ei=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},M)})),ue=new Dn({mutationCb:Ze,adoptedStyleSheetCb:ei}),ce=new yn({mirror:V,mutationCb:Ze,stylesheetManager:ue,recordCrossOriginIframes:F,wrappedEmit:N});for(const M of E||[])M.getMirror&&M.getMirror({nodeMirror:V,crossOriginIframeMirror:ce.crossOriginIframeMirror,crossOriginIframeStyleMirror:ce.crossOriginIframeStyleMirror});const yt=new Nn;lt=new Tn({recordCanvas:B,mutationCb:vr,win:window,blockClass:i,blockSelector:o,mirror:V,sampling:S.canvas,dataURLOptions:v});const et=new vn({mutationCb:Ze,scrollCb:yr,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskInputOptions:pt,dataURLOptions:v,maskTextFn:p,maskInputFn:g,recordCanvas:B,inlineImages:G,sampling:S,slimDOMOptions:mt,iframeManager:ce,stylesheetManager:ue,canvasManager:lt,keepIframeSrcFn:le,processedNodeManager:yt},mirror:V});ze=(M=!1)=>{if(!I)return;N(L({type:O.Meta,data:{href:window.location.href,width:Tt(),height:Rt()}}),M),ue.reset(),et.init(),oe.forEach(D=>D.lock());const K=Vr(document,{mirror:V,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskAllInputs:pt,maskTextFn:p,slimDOM:mt,dataURLOptions:v,recordCanvas:B,inlineImages:G,onSerialize:D=>{At(D,V)&&ce.addIframe(D),Lt(D,V)&&ue.trackLinkElement(D),st(D)&&et.addShadowRoot(D.shadowRoot,document)},onIframeLoad:(D,H)=>{ce.attachIframe(D,H),et.observeAttachShadow(D)},onStylesheetLoad:(D,H)=>{ue.attachLinkElement(D,H)},keepIframeSrcFn:le});if(!K)return console.warn("Failed to snapshot the document");N(L({type:O.FullSnapshot,data:{node:K,initialOffset:xt(window)}}),M),oe.forEach(D=>D.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&ue.adoptStyleSheets(document.adoptedStyleSheets,V.getId(document))};try{const M=[],K=H=>{var de;return _(gn)({mutationCb:Ze,mousemoveCb:(T,vt)=>N(L({type:O.IncrementalSnapshot,data:{source:vt,positions:T}})),mouseInteractionCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},T)})),scrollCb:yr,viewportResizeCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},T)})),inputCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Input},T)})),mediaInteractionCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},T)})),styleSheetRuleCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},T)})),styleDeclarationCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},T)})),canvasMutationCb:vr,fontCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Font},T)})),selectionCb:T=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Selection},T)}))},customElementCb:T=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},T)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:c,maskInputOptions:pt,inlineStylesheet:u,sampling:S,recordDOM:I,recordCanvas:B,inlineImages:G,userTriggeredOnInput:R,collectFonts:j,doc:H,maskInputFn:g,maskTextFn:p,keepIframeSrcFn:le,blockSelector:o,slimDOMOptions:mt,dataURLOptions:v,mirror:V,iframeManager:ce,stylesheetManager:ue,shadowDomManager:et,processedNodeManager:yt,canvasManager:lt,ignoreCSSAttributes:z,plugins:((de=E?.filter(T=>T.observer))===null||de===void 0?void 0:de.map(T=>({observer:T.observer,options:T.options,callback:vt=>N(L({type:O.Plugin,data:{plugin:T.name,payload:vt}}))})))||[]},y)};ce.addLoadListener(H=>{try{M.push(K(H.contentDocument))}catch(de){console.warn(de)}});const D=()=>{ze(),M.push(K(document)),He=!0};return document.readyState==="interactive"||document.readyState==="complete"?D():(M.push(W("DOMContentLoaded",()=>{N(L({type:O.DomContentLoaded,data:{}})),x==="DOMContentLoaded"&&D()})),M.push(W("load",()=>{N(L({type:O.Load,data:{}})),x==="load"&&D()},window))),()=>{M.forEach(H=>H()),yt.destroy(),He=!1,rn()}}catch(M){console.warn(M)}}Oe.addCustomEvent=(e,t)=>{if(!He)throw new Error("please add custom event after start recording");N(L({type:O.Custom,data:{tag:e,payload:t}}))},Oe.freezePage=()=>{oe.forEach(e=>e.freeze())},Oe.takeFullSnapshot=e=>{if(!He)throw new Error("please take full snapshot after start recording");ze(e)},Oe.mirror=V;var tr=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(tr||{}),Y=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(Y||{}),rr={DEBUG:!1,LIB_VERSION:"2.54.0-rc1"},P;if(typeof window>"u"){var nr={hostname:""};P={navigator:{userAgent:""},document:{location:nr,referrer:""},screen:{width:0,height:0},location:nr}}else P=window;var $e=24*60*60*1e3,qe=Array.prototype,An=Function.prototype,ir=Object.prototype,se=qe.slice,Ee=ir.toString,je=ir.hasOwnProperty,ke=P.console,xe=P.navigator,q=P.document,Ge=P.opera,Ve=P.screen,ae=xe.userAgent,ut=An.bind,or=qe.forEach,sr=qe.indexOf,ar=qe.map,Ln=Array.isArray,ct={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},J={log:function(){},warn:function(){},error:function(){},critical:function(){if(!d.isUndefined(ke)&&ke){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{ke.error.apply(ke,e)}catch{d.each(e,function(r){ke.error(r)})}}}},dt=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(J,arguments)}},Je=function(e){return{log:dt(J.log,e),error:dt(J.error,e),critical:dt(J.critical,e)}};d.bind=function(e,t){var r,n;if(ut&&e.bind===ut)return ut.apply(e,se.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=se.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(se.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(se.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(or&&e.forEach===or)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,c="",u="",f=0,m=a,h=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,h=[],Ee.apply(g)==="[object Array]"){for(f=g.length,s=0;s="0"&&t<="9";)g+=t,o();if(t===".")for(g+=".";o()&&t>="0"&&t<="9";)g+=t;if(t==="e"||t==="E")for(g+=t,o(),(t==="-"||t==="+")&&(g+=t,o());t>="0"&&t<="9";)g+=t,o();if(f=+g,!isFinite(f))i("Bad number");else return f},l=function(){var f,g,h="",y;if(t==='"')for(;o();){if(t==='"')return o(),h;if(t==="\\")if(o(),t==="u"){for(y=0,g=0;g<4&&(f=parseInt(o(),16),!!isFinite(f));g+=1)y=y*16+f;h+=String.fromCharCode(y)}else if(typeof r[t]=="string")h+=r[t];else break;else h+=t}i("Bad string")},s=function(){for(;t&&t<=" ";)o()},c=function(){switch(t){case"t":return o("t"),o("r"),o("u"),o("e"),!0;case"f":return o("f"),o("a"),o("l"),o("s"),o("e"),!1;case"n":return o("n"),o("u"),o("l"),o("l"),null}i('Unexpected "'+t+'"')},u,p=function(){var f=[];if(t==="["){if(o("["),s(),t==="]")return o("]"),f;for(;t;){if(f.push(u()),s(),t==="]")return o("]"),f;o(","),s()}}i("Bad array")},m=function(){var f,g={};if(t==="{"){if(o("{"),s(),t==="}")return o("}"),g;for(;t;){if(f=l(),s(),o(":"),Object.hasOwnProperty.call(g,f)&&i('Duplicate key "'+f+'"'),g[f]=u(),s(),t==="}")return o("}"),g;o(","),s()}}i("Bad object")};return u=function(){switch(s(),t){case"{":return m();case"[":return p();case'"':return l();case"-":return a();default:return t>="0"&&t<="9"?a():c()}},function(f){var g;return n=f,e=0,t=" ",g=u(),s(),t&&i("Syntax error"),g}}(),d.base64Encode=function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r,n,i,o,a,l,s,c,u=0,p=0,m="",f=[];if(!e)return e;e=d.utf8Encode(e);do r=e.charCodeAt(u++),n=e.charCodeAt(u++),i=e.charCodeAt(u++),c=r<<16|n<<8|i,o=c>>18&63,a=c>>12&63,l=c>>6&63,s=c&63,f[p++]=t.charAt(o)+t.charAt(a)+t.charAt(l)+t.charAt(s);while(u="0"&&t<="9";)g+=t,o();if(t===".")for(g+=".";o()&&t>="0"&&t<="9";)g+=t;if(t==="e"||t==="E")for(g+=t,o(),(t==="-"||t==="+")&&(g+=t,o());t>="0"&&t<="9";)g+=t,o();if(h=+g,!isFinite(h))i("Bad number");else return h},l=function(){var h,g,p="",y;if(t==='"')for(;o();){if(t==='"')return o(),p;if(t==="\\")if(o(),t==="u"){for(y=0,g=0;g<4&&(h=parseInt(o(),16),!!isFinite(h));g+=1)y=y*16+h;p+=String.fromCharCode(y)}else if(typeof r[t]=="string")p+=r[t];else break;else p+=t}i("Bad string")},s=function(){for(;t&&t<=" ";)o()},c=function(){switch(t){case"t":return o("t"),o("r"),o("u"),o("e"),!0;case"f":return o("f"),o("a"),o("l"),o("s"),o("e"),!1;case"n":return o("n"),o("u"),o("l"),o("l"),null}i('Unexpected "'+t+'"')},u,f=function(){var h=[];if(t==="["){if(o("["),s(),t==="]")return o("]"),h;for(;t;){if(h.push(u()),s(),t==="]")return o("]"),h;o(","),s()}}i("Bad array")},m=function(){var h,g={};if(t==="{"){if(o("{"),s(),t==="}")return o("}"),g;for(;t;){if(h=l(),s(),o(":"),Object.hasOwnProperty.call(g,h)&&i('Duplicate key "'+h+'"'),g[h]=u(),s(),t==="}")return o("}"),g;o(","),s()}}i("Bad object")};return u=function(){switch(s(),t){case"{":return m();case"[":return f();case'"':return l();case"-":return a();default:return t>="0"&&t<="9"?a():c()}},function(h){var g;return n=h,e=0,t=" ",g=u(),s(),t&&i("Syntax error"),g}}(),d.base64Encode=function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r,n,i,o,a,l,s,c,u=0,f=0,m="",h=[];if(!e)return e;e=d.utf8Encode(e);do r=e.charCodeAt(u++),n=e.charCodeAt(u++),i=e.charCodeAt(u++),c=r<<16|n<<8|i,o=c>>18&63,a=c>>12&63,l=c>>6&63,s=c&63,h[f++]=t.charAt(o)+t.charAt(a)+t.charAt(l)+t.charAt(s);while(u127&&a<2048?l=String.fromCharCode(a>>6|192,a&63|128):l=String.fromCharCode(a>>12|224,a>>6&63|128,a&63|128),l!==null&&(n>r&&(t+=e.substring(r,n)),t+=l,r=n=o+1)}return n>r&&(t+=e.substring(r,e.length)),t},d.UUID=function(){var e=function(){var n=1*new Date,i;if(B.performance&&B.performance.now)i=B.performance.now();else for(i=0;n==1*new Date;)i++;return n.toString(16)+Math.floor(i).toString(16)},t=function(){return Math.random().toString(16).replace(".","")},r=function(){var n=ie,i,o,a=[],l=0;function s(c,u){var p,m=0;for(p=0;p=4&&(l=s(l,a),a=[]);return a.length>0&&(l=s(l,a)),l.toString(16)};return function(){var n=(ze.height*ze.width).toString(16);return e()+"-"+t()+"-"+r()+"-"+n+"-"+e()}}();var Zt=["ahrefsbot","ahrefssiteaudit","baiduspider","bingbot","bingpreview","chrome-lighthouse","facebookexternal","petalbot","pinterest","screaming frog","yahoo! slurp","yandexbot","adsbot-google","apis-google","duplexweb-google","feedfetcher-google","google favicon","google web preview","google-read-aloud","googlebot","googleweblight","mediapartners-google","storebot-google"];d.isBlockedUA=function(e){var t;for(e=e.toLowerCase(),t=0;t=0}function n(i){if(!G.getElementsByTagName)return[];var o=i.split(" "),a,l,s,c,u,p,m,f,g,h,y=[G];for(p=0;p-1){l=a.split("#"),s=l[0];var w=l[1],v=G.getElementById(w);if(!v||s&&v.nodeName.toLowerCase()!=s)return[];y=[v];continue}if(a.indexOf(".")>-1){l=a.split("."),s=l[0];var S=l[1];for(s||(s="*"),c=[],u=0,m=0;m-1};break;default:k=function(T){return T.getAttribute(M)}}for(y=[],h=0,m=0;m=3?t[2]:""},currentUrl:function(){return B.location.href},properties:function(e){return typeof e!="object"&&(e={}),d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ie,Oe.vendor,He),$referrer:G.referrer,$referring_domain:d.info.referringDomain(G.referrer),$device:d.info.device(ie)}),{$current_url:d.info.currentUrl(),$browser_version:d.info.browserVersion(ie,Oe.vendor,He),$screen_height:ze.height,$screen_width:ze.width,mp_lib:"web",$lib_version:Mn.LIB_VERSION,$insert_id:er(),time:d.timestamp()/1e3},d.strip_empty_properties(e))},people_properties:function(){return d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ie,Oe.vendor,He)}),{$browser_version:d.info.browserVersion(ie,Oe.vendor,He)})},mpPageViewProperties:function(){return d.strip_empty_properties({current_page_title:G.title,current_domain:B.location.hostname,current_url_path:B.location.pathname,current_url_protocol:B.location.protocol,current_url_search:B.location.search})}};var er=function(e){var t=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return e?t.substring(0,e):t},Tn=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Rn=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,tr=function(e){var t=Rn,r=e.split("."),n=r[r.length-1];(n.length>4||n==="com"||n==="org")&&(t=Tn);var i=e.match(t);return i?i[0]:""},it=null,ot=null;typeof JSON<"u"&&(it=JSON.stringify,ot=JSON.parse),it=it||d.JSONEncode,ot=ot||d.JSONDecode,d.toArray=d.toArray,d.isObject=d.isObject,d.JSONEncode=d.JSONEncode,d.JSONDecode=d.JSONDecode,d.isBlockedUA=d.isBlockedUA,d.isEmptyObject=d.isEmptyObject,d.info=d.info,d.info.device=d.info.device,d.info.browser=d.info.browser,d.info.browserVersion=d.info.browserVersion,d.info.properties=d.info.properties;var Nn="__mp_opt_in_out_";function Dn(e,t){if(Bn(t))return J.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),!0;var r=Fn(e,t)==="0";return r&&J.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."),r}function An(e){return Wn(e,function(t){return this.get_config(t)})}function Ln(e){return e=e||{},e.persistenceType==="localStorage"?d.localStorage:d.cookie}function Pn(e,t){return t=t||{},(t.persistencePrefix||Nn)+e}function Fn(e,t){return Ln(t).get(Pn(e,t))}function Bn(e){if(e&&e.ignoreDnt)return!1;var t=e&&e.window||B,r=t.navigator||{},n=!1;return d.each([r.doNotTrack,r.msDoNotTrack,t.doNotTrack],function(i){d.includes([!0,1,"1","yes"],i)&&(n=!0)}),n}function Wn(e,t){return function(){var r=!1;try{var n=t.call(this,"token"),i=t.call(this,"ignore_dnt"),o=t.call(this,"opt_out_tracking_persistence_type"),a=t.call(this,"opt_out_tracking_cookie_prefix"),l=t.call(this,"window");n&&(r=Dn(n,{ignoreDnt:i,persistenceType:o,persistencePrefix:a,window:l}))}catch(c){J.error("Unexpected error when checking tracking opt-out status: "+c)}if(!r)return e.apply(this,arguments);var s=arguments[arguments.length-1];typeof s=="function"&&s(0)}}var st=On("recorder"),rr=window.CompressionStream,Q=function(e){this._mixpanel=e,this._stopRecording=null,this.recEvents=[],this.seqNo=0,this.replayId=null,this.replayStartTime=null,this.batchStartTime=null,this.replayLengthMs=0,this.sendBatchId=null,this.idleTimeoutId=null,this.maxTimeoutId=null,this.recordMaxMs=Be};Q.prototype.get_config=function(e){return this._mixpanel.get_config(e)},Q.prototype.startRecording=function(){if(this._stopRecording!==null){st.log("Recording already in progress, skipping startRecording.");return}this.recordMaxMs=this.get_config("record_max_ms"),this.recordMaxMs>Be&&(this.recordMaxMs=Be,st.critical("record_max_ms cannot be greater than "+Be+"ms. Capping value.")),this.recEvents=[],this.seqNo=0,this.startDate=new Date,this.replayStartTime=this.startDate.getTime(),this.batchStartTime=this.replayStartTime,this.replayId=d.UUID(),this.replayLengthMs=0;var e=d.bind(function(){clearTimeout(this.idleTimeoutId),this.idleTimeoutId=setTimeout(d.bind(function(){st.log("Idle timeout reached, restarting recording."),this.resetRecording()},this),this.get_config("record_idle_timeout_ms"))},this);this._stopRecording=Me({emit:d.bind(function(t){this.recEvents.push(t),this.replayLengthMs=new Date().getTime()-this.replayStartTime,e()},this),maskAllInputs:!0,maskTextSelector:this.get_config("record_mask_text_selector"),blockSelector:this.get_config("record_block_selector"),maskTextClass:this.get_config("record_mask_text_class"),blockClass:this.get_config("record_block_class")}),e(),this.sendBatchId=setInterval(d.bind(this.flushEventsWithOptOut,this),1e4),this.maxTimeoutId=setTimeout(d.bind(this.resetRecording,this),this.recordMaxMs)},Q.prototype.resetRecording=function(){this.stopRecording(),this.startRecording()},Q.prototype.stopRecording=function(){this._stopRecording!==null&&(this._stopRecording(),this._stopRecording=null),this._flushEvents(),this.replayId=null,clearInterval(this.sendBatchId),clearTimeout(this.idleTimeoutId),clearTimeout(this.maxTimeoutId)},Q.prototype.flushEventsWithOptOut=function(){this._flushEvents(d.bind(this._onOptOut,this))},Q.prototype._onOptOut=function(e){e===0&&(this.recEvents=[],this.stopRecording())},Q.prototype._sendRequest=function(e,t){window.fetch(this.get_config("api_host")+"/"+this.get_config("api_routes").record+"?"+new URLSearchParams(e),{method:"POST",headers:{Authorization:"Basic "+btoa(this.get_config("token")+":"),"Content-Type":"application/octet-stream"},body:t})},Q.prototype._flushEvents=An(function(){var e=this.recEvents.length;if(e>0){var t={distinct_id:String(this._mixpanel.get_distinct_id()),seq:this.seqNo++,batch_start_time:this.batchStartTime/1e3,replay_id:this.replayId,replay_length_ms:this.replayLengthMs,replay_start_time:this.replayStartTime/1e3},r=d.JSONEncode(this.recEvents),n=this._mixpanel.get_property("$device_id");n&&(t.$device_id=n);var i=this._mixpanel.get_property("$user_id");if(i&&(t.$user_id=i),this.recEvents=this.recEvents.slice(e),this.batchStartTime=new Date().getTime(),rr){var o=new Blob([r],{type:"application/json"}).stream(),a=o.pipeThrough(new rr("gzip"));new Response(a).blob().then(d.bind(function(l){t.format="gzip",this._sendRequest(t,l)},this))}else t.format="body",this._sendRequest(t,r)}}),window.__mp_recorder=Q})(); +`);var t="",r,n,i=0,o;for(r=n=0,i=e.length,o=0;o127&&a<2048?l=String.fromCharCode(a>>6|192,a&63|128):l=String.fromCharCode(a>>12|224,a>>6&63|128,a&63|128),l!==null&&(n>r&&(t+=e.substring(r,n)),t+=l,r=n=o+1)}return n>r&&(t+=e.substring(r,e.length)),t},d.UUID=function(){var e=function(){var n=1*new Date,i;if(P.performance&&P.performance.now)i=P.performance.now();else for(i=0;n==1*new Date;)i++;return n.toString(16)+Math.floor(i).toString(16)},t=function(){return Math.random().toString(16).replace(".","")},r=function(){var n=ae,i,o,a=[],l=0;function s(c,u){var f,m=0;for(f=0;f=4&&(l=s(l,a),a=[]);return a.length>0&&(l=s(l,a)),l.toString(16)};return function(){var n=(Ve.height*Ve.width).toString(16);return e()+"-"+t()+"-"+r()+"-"+n+"-"+e()}}();var lr=["ahrefsbot","ahrefssiteaudit","baiduspider","bingbot","bingpreview","chrome-lighthouse","facebookexternal","petalbot","pinterest","screaming frog","yahoo! slurp","yandexbot","adsbot-google","apis-google","duplexweb-google","feedfetcher-google","google favicon","google web preview","google-read-aloud","googlebot","googleweblight","mediapartners-google","storebot-google"];d.isBlockedUA=function(e){var t;for(e=e.toLowerCase(),t=0;t=0}function n(i){if(!q.getElementsByTagName)return[];var o=i.split(" "),a,l,s,c,u,f,m,h,g,p,y=[q];for(f=0;f-1){l=a.split("#"),s=l[0];var w=l[1],S=q.getElementById(w);if(!S||s&&S.nodeName.toLowerCase()!=s)return[];y=[S];continue}if(a.indexOf(".")>-1){l=a.split("."),s=l[0];var v=l[1];for(s||(s="*"),c=[],u=0,m=0;m-1};break;default:x=function(R){return R.getAttribute(I)}}for(y=[],p=0,m=0;m=3?t[2]:""},currentUrl:function(){return P.location.href},properties:function(e){return typeof e!="object"&&(e={}),d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ae,xe.vendor,Ge),$referrer:q.referrer,$referring_domain:d.info.referringDomain(q.referrer),$device:d.info.device(ae)}),{$current_url:d.info.currentUrl(),$browser_version:d.info.browserVersion(ae,xe.vendor,Ge),$screen_height:Ve.height,$screen_width:Ve.width,mp_lib:"web",$lib_version:rr.LIB_VERSION,$insert_id:ht(),time:d.timestamp()/1e3},d.strip_empty_properties(e))},people_properties:function(){return d.extend(d.strip_empty_properties({$os:d.info.os(),$browser:d.info.browser(ae,xe.vendor,Ge)}),{$browser_version:d.info.browserVersion(ae,xe.vendor,Ge)})},mpPageViewProperties:function(){return d.strip_empty_properties({current_page_title:q.title,current_domain:P.location.hostname,current_url_path:P.location.pathname,current_url_protocol:P.location.protocol,current_url_search:P.location.search})}};var ht=function(e){var t=Math.random().toString(36).substring(2,10)+Math.random().toString(36).substring(2,10);return e?t.substring(0,e):t},Wn=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Un=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,ur=function(e){var t=Un,r=e.split("."),n=r[r.length-1];(n.length>4||n==="com"||n==="org")&&(t=Wn);var i=e.match(t);return i?i[0]:""},Ke=null,Ye=null;typeof JSON<"u"&&(Ke=JSON.stringify,Ye=JSON.parse),Ke=Ke||d.JSONEncode,Ye=Ye||d.JSONDecode,d.toArray=d.toArray,d.isObject=d.isObject,d.JSONEncode=d.JSONEncode,d.JSONDecode=d.JSONDecode,d.isBlockedUA=d.isBlockedUA,d.isEmptyObject=d.isEmptyObject,d.info=d.info,d.info.device=d.info.device,d.info.browser=d.info.browser,d.info.browserVersion=d.info.browserVersion,d.info.properties=d.info.properties;var zn="__mp_opt_in_out_";function Hn(e,t){if(Vn(t))return J.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'),!0;var r=Gn(e,t)==="0";return r&&J.warn("You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data."),r}function $n(e){return Jn(e,function(t){return this.get_config(t)})}function qn(e){return e=e||{},e.persistenceType==="localStorage"?d.localStorage:d.cookie}function jn(e,t){return t=t||{},(t.persistencePrefix||zn)+e}function Gn(e,t){return qn(t).get(jn(e,t))}function Vn(e){if(e&&e.ignoreDnt)return!1;var t=e&&e.window||P,r=t.navigator||{},n=!1;return d.each([r.doNotTrack,r.msDoNotTrack,t.doNotTrack],function(i){d.includes([!0,1,"1","yes"],i)&&(n=!0)}),n}function Jn(e,t){return function(){var r=!1;try{var n=t.call(this,"token"),i=t.call(this,"ignore_dnt"),o=t.call(this,"opt_out_tracking_persistence_type"),a=t.call(this,"opt_out_tracking_cookie_prefix"),l=t.call(this,"window");n&&(r=Hn(n,{ignoreDnt:i,persistenceType:o,persistencePrefix:a,window:l}))}catch(c){J.error("Unexpected error when checking tracking opt-out status: "+c)}if(!r)return e.apply(this,arguments);var s=arguments[arguments.length-1];typeof s=="function"&&s(0)}}var Xn=Je("lock"),cr=function(e,t){t=t||{},this.storageKey=e,this.storage=t.storage||window.localStorage,this.pollIntervalMS=t.pollIntervalMS||100,this.timeoutMS=t.timeoutMS||2e3};cr.prototype.withLock=function(e,t,r){!r&&typeof t!="function"&&(r=t,t=null);var n=r||new Date().getTime()+"|"+Math.random(),i=new Date().getTime(),o=this.storageKey,a=this.pollIntervalMS,l=this.timeoutMS,s=this.storage,c=o+":X",u=o+":Y",f=o+":Z",m=function(S){t&&t(S)},h=function(S){if(new Date().getTime()-i>l){Xn.error("Timeout waiting for mutex on "+o+"; clearing lock. ["+n+"]"),s.removeItem(f),s.removeItem(u),y();return}setTimeout(function(){try{S()}catch(v){m(v)}},a*(Math.random()+.1))},g=function(S,v){S()?v():h(function(){g(S,v)})},p=function(){var S=s.getItem(u);if(S&&S!==n)return!1;if(s.setItem(u,n),s.getItem(u)===n)return!0;if(!Xe(s,!0))throw new Error("localStorage support dropped while acquiring lock");return!1},y=function(){s.setItem(c,n),g(p,function(){if(s.getItem(c)===n){w();return}h(function(){if(s.getItem(u)!==n){y();return}g(function(){return!s.getItem(f)},w)})})},w=function(){s.setItem(f,"1");try{e()}finally{s.removeItem(f),s.getItem(u)===n&&s.removeItem(u),s.getItem(c)===n&&s.removeItem(c)}};try{if(Xe(s,!0))y();else throw new Error("localStorage support check failed")}catch(S){m(S)}};var dr=Je("batch"),re=function(e,t){t=t||{},this.storageKey=e,this.storage=t.storage||window.localStorage,this.reportError=t.errorReporter||d.bind(dr.error,dr),this.lock=new cr(e,{storage:this.storage}),this.usePersistence=t.usePersistence,this.pid=t.pid||null,this.memQueue=[]};re.prototype.enqueue=function(e,t,r){var n={id:ht(),flushAfter:new Date().getTime()+t*2,payload:e};this.usePersistence?this.lock.withLock(d.bind(function(){var o;try{var a=this.readFromStorage();a.push(n),o=this.saveToStorage(a),o&&this.memQueue.push(n)}catch{this.reportError("Error enqueueing item",e),o=!1}r&&r(o)},this),d.bind(function(o){this.reportError("Error acquiring storage lock",o),r&&r(!1)},this),this.pid):(this.memQueue.push(n),r&&r(!0))},re.prototype.fillBatch=function(e){var t=this.memQueue.slice(0,e);if(this.usePersistence&&t.lengtho.flushAfter&&!n[o.id]&&(o.orphaned=!0,t.push(o),t.length>=e))break}}}return t};var fr=function(e,t){var r=[];return d.each(e,function(n){n.id&&!t[n.id]&&r.push(n)}),r};re.prototype.removeItemsByID=function(e,t){var r={};if(d.each(e,function(i){r[i]=!0}),this.memQueue=fr(this.memQueue,r),!this.usePersistence)t&&t(!0);else{var n=d.bind(function(){var i;try{var o=this.readFromStorage();if(o=fr(o,r),i=this.saveToStorage(o),i){o=this.readFromStorage();for(var a=0;a5&&(this.reportError("[dupe] item ID sent too many times, not sending",{item:u,batchSize:i.length,timesSent:this.itemIdsSentSuccessfully[h]}),m=!1):this.reportError("[dupe] found item with no ID",{item:u}),m&&a.push(f)}l[u.id]=f},this),a.length<1){this.resetFlush();return}this.requestInProgress=!0;var s=d.bind(function(u){this.requestInProgress=!1;try{var f=!1;if(e.unloading)this.queue.updatePayloads(l);else if(d.isObject(u)&&u.error==="timeout"&&new Date().getTime()-r>=t)this.reportError("Network timeout; retrying"),this.flush();else if(d.isObject(u)&&(u.httpStatusCode>=500||u.httpStatusCode===429||u.error==="timeout")){var m=this.flushInterval*2;u.retryAfter&&(m=parseInt(u.retryAfter,10)*1e3||m),m=Math.min(Kn,m),this.reportError("Error; retry in "+m+" ms"),this.scheduleFlush(m)}else if(d.isObject(u)&&u.httpStatusCode===413)if(i.length>1){var h=Math.max(1,Math.floor(n/2));this.batchSize=Math.min(this.batchSize,h,i.length-1),this.reportError("413 response; reducing batch size to "+this.batchSize),this.resetFlush()}else this.reportError("Single-event request too large; dropping",i),this.resetBatchSize(),f=!0;else f=!0;f&&(this.queue.removeItemsByID(d.map(i,function(g){return g.id}),d.bind(function(g){g?(this.consecutiveRemovalFailures=0,this.flushOnlyOnInterval&&!o?this.resetFlush():this.flush()):(this.reportError("Failed to remove items from queue"),++this.consecutiveRemovalFailures>5?(this.reportError("Too many queue failures; disabling batching system."),this.stopAllBatching()):this.resetFlush())},this)),d.each(i,d.bind(function(g){var p=g.id;p?(this.itemIdsSentSuccessfully[p]=this.itemIdsSentSuccessfully[p]||0,this.itemIdsSentSuccessfully[p]++,this.itemIdsSentSuccessfully[p]>5&&this.reportError("[dupe] item ID sent too many times",{item:g,batchSize:i.length,timesSent:this.itemIdsSentSuccessfully[p]})):this.reportError("[dupe] found item with no ID while removing",{item:g})},this)))}catch(g){this.reportError("Error handling API response",g),this.resetFlush()}},this),c={method:"POST",verbose:!0,ignore_json_errors:!0,timeout_ms:t};e.unloading&&(c.transport="sendBeacon"),Re.log("MIXPANEL REQUEST:",a),this.sendRequest(a,c,s)}catch(u){this.reportError("Error flushing request queue",u),this.resetFlush()}},Q.prototype.reportError=function(e,t){if(Re.error.apply(Re.error,arguments),this.errorReporter)try{t instanceof Error||(t=new Error(e)),this.errorReporter(e,t)}catch(r){Re.error(r)}};var Se=Je("recorder"),pr=P.CompressionStream,Yn={batch_size:1e3,batch_flush_interval_ms:10*1e3,batch_request_timeout_ms:90*1e3,batch_autostart:!0},Qn=new Set([Y.MouseMove,Y.MouseInteraction,Y.Scroll,Y.ViewportResize,Y.Input,Y.TouchMove,Y.MediaInteraction,Y.Drag,Y.Selection]);function Zn(e){return e.type===tr.IncrementalSnapshot&&Qn.has(e.source)}var X=function(e){this._mixpanel=e,this._stopRecording=null,this.recEvents=[],this.seqNo=0,this.replayId=null,this.replayStartTime=null,this.sendBatchId=null,this.idleTimeoutId=null,this.maxTimeoutId=null,this.recordMaxMs=$e,this._initBatcher()};X.prototype._initBatcher=function(){this.batcher=new Q("__mprec",{libConfig:Yn,sendRequestFunc:d.bind(this.flushEventsWithOptOut,this),errorReporter:d.bind(this.reportError,this),flushOnlyOnInterval:!0,usePersistence:!1})},X.prototype.get_config=function(e){return this._mixpanel.get_config(e)},X.prototype.startRecording=function(){if(this._stopRecording!==null){Se.log("Recording already in progress, skipping startRecording.");return}this.recordMaxMs=this.get_config("record_max_ms"),this.recordMaxMs>$e&&(this.recordMaxMs=$e,Se.critical("record_max_ms cannot be greater than "+$e+"ms. Capping value.")),this.recEvents=[],this.seqNo=0,this.replayStartTime=null,this.replayId=d.UUID(),this.batcher.start();var e=d.bind(function(){clearTimeout(this.idleTimeoutId),this.idleTimeoutId=setTimeout(d.bind(function(){Se.log("Idle timeout reached, restarting recording."),this.resetRecording()},this),this.get_config("record_idle_timeout_ms"))},this);this._stopRecording=Oe({emit:d.bind(function(t){this.batcher.enqueue(t),Zn(t)&&e()},this),blockClass:this.get_config("record_block_class"),blockSelector:this.get_config("record_block_selector"),collectFonts:this.get_config("record_collect_fonts"),inlineImages:this.get_config("record_inline_images"),maskAllInputs:!0,maskTextClass:this.get_config("record_mask_text_class"),maskTextSelector:this.get_config("record_mask_text_selector")}),e(),this.maxTimeoutId=setTimeout(d.bind(this.resetRecording,this),this.recordMaxMs)},X.prototype.resetRecording=function(){this.stopRecording(),this.startRecording()},X.prototype.stopRecording=function(){this._stopRecording!==null&&(this._stopRecording(),this._stopRecording=null),this.batcher.flush(),this.replayId=null,clearTimeout(this.idleTimeoutId),clearTimeout(this.maxTimeoutId)},X.prototype.flushEventsWithOptOut=function(e,t,r){this._flushEvents(e,t,r,d.bind(this._onOptOut,this))},X.prototype._onOptOut=function(e){e===0&&(this.recEvents=[],this.stopRecording())},X.prototype._sendRequest=function(e,t,r){var n=d.bind(function(i,o){i.status===200&&this.seqNo++,r({status:0,httpStatusCode:i.status,responseBody:o,retryAfter:i.headers.get("Retry-After")})},this);P.fetch(this.get_config("api_host")+"/"+this.get_config("api_routes").record+"?"+new URLSearchParams(e),{method:"POST",headers:{Authorization:"Basic "+btoa(this.get_config("token")+":"),"Content-Type":"application/octet-stream"},body:t}).then(function(i){i.json().then(function(o){n(i,o)}).catch(function(o){r({error:o})})}).catch(function(i){r({error:i})})},X.prototype._flushEvents=$n(function(e,t,r){const n=e.length;if(n>0){var i=e[0].timestamp;this.seqNo===0&&(this.replayStartTime=i);var o=e[n-1].timestamp-this.replayStartTime,a={distinct_id:String(this._mixpanel.get_distinct_id()),seq:this.seqNo,batch_start_time:i/1e3,replay_id:this.replayId,replay_length_ms:o,replay_start_time:this.replayStartTime/1e3},l=d.JSONEncode(e),s=this._mixpanel.get_property("$device_id");s&&(a.$device_id=s);var c=this._mixpanel.get_property("$user_id");if(c&&(a.$user_id=c),pr){var u=new Blob([l],{type:"application/json"}).stream(),f=u.pipeThrough(new pr("gzip"));new Response(f).blob().then(d.bind(function(m){a.format="gzip",this._sendRequest(a,m,r)},this))}else a.format="body",this._sendRequest(a,l,r)}}),X.prototype.reportError=function(e,t){Se.error.apply(Se.error,arguments);try{!t&&!(e instanceof Error)&&(e=new Error(e)),this.get_config("error_reporter")(e,t)}catch(r){Se.error(r)}},P.__mp_recorder=X})(); diff --git a/dist/mixpanel-with-async-recorder.cjs.js b/dist/mixpanel-with-async-recorder.cjs.js index ba98f333..10f4359c 100644 --- a/dist/mixpanel-with-async-recorder.cjs.js +++ b/dist/mixpanel-with-async-recorder.cjs.js @@ -2,7 +2,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -2051,6 +2051,7 @@ var RequestQueue = function(storageKey, options) { this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2075,29 +2076,36 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2108,7 +2116,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2157,61 +2165,67 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2239,25 +2253,32 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2300,7 +2321,10 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2318,7 +2342,8 @@ var RequestBatcher = function(storageKey, options) { this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2335,6 +2360,11 @@ var RequestBatcher = function(storageKey, options) { // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2419,6 +2449,9 @@ RequestBatcher.prototype.flush = function(options) { var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2486,22 +2519,17 @@ RequestBatcher.prototype.flush = function(options) { this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2525,7 +2553,11 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2573,7 +2605,6 @@ RequestBatcher.prototype.flush = function(options) { } logger.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -4206,7 +4237,9 @@ var DEFAULT_CONFIG = { 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4741,7 +4774,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4841,6 +4875,7 @@ MixpanelLib.prototype.init_batchers = function() { attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4852,8 +4887,8 @@ MixpanelLib.prototype.init_batchers = function() { beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); diff --git a/dist/mixpanel.amd.js b/dist/mixpanel.amd.js index f00fce89..9cb22cc3 100644 --- a/dist/mixpanel.amd.js +++ b/dist/mixpanel.amd.js @@ -1,8 +1,4515 @@ define((function () { 'use strict'; + var NodeType; + (function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; + })(NodeType || (NodeType = {})); + + function isElement(n) { + return n.nodeType === n.ELEMENT_NODE; + } + function isShadowRoot(n) { + const host = n === null || n === void 0 ? void 0 : n.host; + return Boolean((host === null || host === void 0 ? void 0 : host.shadowRoot) === n); + } + function isNativeShadowDom(shadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; + } + function fixBrowserCompatibilityIssuesInCSS(cssText) { + if (cssText.includes(' background-clip: text;') && + !cssText.includes(' -webkit-background-clip: text;')) { + cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;'); + } + return cssText; + } + function escapeImportStatement(rule) { + const { cssText } = rule; + if (cssText.split('"').length < 3) + return cssText; + const statement = ['@import', `url(${JSON.stringify(rule.href)})`]; + if (rule.layerName === '') { + statement.push(`layer`); + } + else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); + } + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); + } + if (rule.media.length) { + statement.push(rule.media.mediaText); + } + return statement.join(' ') + ';'; + } + function stringifyStylesheet(s) { + try { + const rules = s.rules || s.cssRules; + return rules + ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) + : null; + } + catch (error) { + return null; + } + } + function stringifyRule(rule) { + let importStringified; + if (isCSSImportRule(rule)) { + try { + importStringified = + stringifyStylesheet(rule.styleSheet) || + escapeImportStatement(rule); + } + catch (error) { + } + } + else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + return fixSafariColons(rule.cssText); + } + return importStringified || rule.cssText; + } + function fixSafariColons(cssStringified) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); + } + function isCSSImportRule(rule) { + return 'styleSheet' in rule; + } + function isCSSStyleRule(rule) { + return 'selectorText' in rule; + } + class Mirror { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + } + function createMirror() { + return new Mirror(); + } + function maskInputValue({ element, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + const actualType = type && toLowerCase(type); + if (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType])) { + if (maskInputFn) { + text = maskInputFn(text, element); + } + else { + text = '*'.repeat(text.length); + } + } + return text; + } + function toLowerCase(str) { + return str.toLowerCase(); + } + const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; + function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; + } + function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? + toLowerCase(type) + : null; + } + function extractFileExtension(path, baseURL) { + var _a; + let url; + try { + url = new URL(path, baseURL !== null && baseURL !== void 0 ? baseURL : window.location.href); + } + catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; + const match = url.pathname.match(regex); + return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null; + } + + let _id = 1; + const tagNameRegex = new RegExp('[^a-z0-9-_:]'); + const IGNORED_NODE = -2; + function genId() { + return _id++; + } + function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; + } + function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; + } + let canvasService; + let canvasCtx; + const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; + const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; + const URL_WWW_MATCH = /^www\..*/i; + const DATA_URI = /^(data:)([^,]*),(.*)/i; + function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); + } + const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; + const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; + function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + const output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); + } + function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; + } + function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); + } + function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; + } + function transformAttribute(doc, tagName, name, value) { + if (!value) { + return value; + } + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + return value; + } + function ignoreAttribute(tagName, name, _value) { + return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; + } + function _isBlockedElement(element, blockClass, blockSelector) { + try { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = element.classList.length; eIndex--;) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + } + catch (e) { + } + return false; + } + function classMatchesRegex(node, regex, checkAncestors) { + if (!node) + return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + for (let eIndex = node.classList.length; eIndex--;) { + const className = node.classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + function needMaskingText(node, maskTextClass, maskTextSelector, checkAncestors) { + try { + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + if (el === null) + return false; + if (typeof maskTextClass === 'string') { + if (checkAncestors) { + if (el.closest(`.${maskTextClass}`)) + return true; + } + else { + if (el.classList.contains(maskTextClass)) + return true; + } + } + else { + if (classMatchesRegex(el, maskTextClass, checkAncestors)) + return true; + } + if (maskTextSelector) { + if (checkAncestors) { + if (el.closest(maskTextSelector)) + return true; + } + else { + if (el.matches(maskTextSelector)) + return true; + } + } + } + catch (e) { + } + return false; + } + function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); + } + iframeEl.addEventListener('load', listener); + } + function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; + } + catch (error) { + return; + } + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + } + function serializeNode(n, options) { + const { doc, mirror, blockClass, blockSelector, needsMask, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options; + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: n.compatMode, + }; + } + else { + return { + type: NodeType.Document, + childNodes: [], + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n, { + needsMask, + maskTextFn, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } + } + function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; + } + function serializeTextNode(n, options) { + var _a; + const { needsMask, maskTextFn, rootId } = options; + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStylesheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (!isStyle && !isScript && textContent && needsMask) { + textContent = maskTextFn + ? maskTextFn(textContent, n.parentElement) + : textContent.replace(/[\S]/g, '*'); + } + return { + type: NodeType.Text, + textContent: textContent || '', + isStyle, + rootId, + }; + } + function serializeElementNode(n, options) { + const { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions = {}, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, } = options; + const needBlock = _isBlockedElement(n, blockClass, blockSelector); + const tagName = getValidTagName(n); + let attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = stringifyStylesheet(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = n.value; + const checked = n.checked; + if (attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value) { + attributes.value = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } + else if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + image.removeEventListener('load', recordInlineImage); + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.addEventListener('load', recordInlineImage); + } + if (tagName === 'audio' || tagName === 'video') { + const mediaAttributes = attributes; + mediaAttributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + mediaAttributes.rr_mediaCurrentTime = n.currentTime; + mediaAttributes.rr_mediaPlaybackRate = n.playbackRate; + mediaAttributes.rr_mediaMuted = n.muted; + mediaAttributes.rr_mediaLoop = n.loop; + mediaAttributes.rr_mediaVolume = n.volume; + } + if (!newlyAddedElement) { + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + let isCustomElement; + try { + if (customElements.get(tagName)) + isCustomElement = true; + } + catch (e) { + } + return { + type: NodeType.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + isCustom: isCustomElement, + }; + } + function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } + } + function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + return true; + } + else if (sn.type === NodeType.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + extractFileExtension(sn.attributes.href) === 'js'))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; + } + function serializeNodeWithId(n, options) { + const { doc, mirror, blockClass, blockSelector, maskTextClass, maskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options; + let { needsMask } = options; + let { preserveWhiteSpace = true } = options; + if (!needsMask && + n.childNodes) { + const checkAncestors = needsMask === undefined; + needsMask = needMaskingText(n, maskTextClass, maskTextSelector, checkAncestors); + } + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if (mirror.hasNode(n)) { + id = mirror.getId(n); + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + mirror.add(n, serializedNode); + if (id === IGNORED_NODE) { + return null; + } + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + const shadowRoot = n.shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType.Document || + serializedNode.type === NodeType.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }; + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'textarea' && + serializedNode.attributes.value !== undefined) ; + else { + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css'))) { + onceStylesheetLoaded(n, () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedLinkNode) { + onStylesheetLoad(n, serializedLinkNode); + } + } + }, stylesheetLoadTimeout); + } + return serializedNode; + } + function snapshot(n, options) { + const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + }); + } + + function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); + } + const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; + let _mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + }; + if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); + } + function throttle(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (...args) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; + } + function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); + } + function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { + }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { + }; + } + } + let nowTimestamp = Date.now; + if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) { + nowTimestamp = () => new Date().getTime(); + } + function getWindowScroll(win) { + var _a, _b, _c, _d, _e, _f; + const doc = win.document; + return { + left: doc.scrollingElement + ? doc.scrollingElement.scrollLeft + : win.pageXOffset !== undefined + ? win.pageXOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollLeft) || + ((_b = (_a = doc === null || doc === void 0 ? void 0 : doc.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || + ((_c = doc === null || doc === void 0 ? void 0 : doc.body) === null || _c === void 0 ? void 0 : _c.scrollLeft) || + 0, + top: doc.scrollingElement + ? doc.scrollingElement.scrollTop + : win.pageYOffset !== undefined + ? win.pageYOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollTop) || + ((_e = (_d = doc === null || doc === void 0 ? void 0 : doc.body) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.scrollTop) || + ((_f = doc === null || doc === void 0 ? void 0 : doc.body) === null || _f === void 0 ? void 0 : _f.scrollTop) || + 0, + }; + } + function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); + } + function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); + } + function closestElementOfNode(node) { + if (!node) { + return null; + } + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + return el; + } + function isBlocked(node, blockClass, blockSelector, checkAncestors) { + if (!node) { + return false; + } + const el = closestElementOfNode(node); + if (!el) { + return false; + } + try { + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) + return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) + return true; + } + else { + if (classMatchesRegex(el, blockClass, checkAncestors)) + return true; + } + } + catch (e) { + } + if (blockSelector) { + if (el.matches(blockSelector)) + return true; + if (checkAncestors && el.closest(blockSelector) !== null) + return true; + } + return false; + } + function isSerialized(n, mirror) { + return mirror.getId(n) !== -1; + } + function isIgnored(n, mirror) { + return mirror.getId(n) === IGNORED_NODE; + } + function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); + } + function legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); + } + function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = (...args) => { + let node = args[0]; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } + } + function isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); + } + function isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); + } + function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); + } + class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + } + getId(stylesheet) { + var _a; + return (_a = this.styleIDMap.get(stylesheet)) !== null && _a !== void 0 ? _a : -1; + } + has(stylesheet) { + return this.styleIDMap.has(stylesheet); + } + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; + } + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + getStyle(id) { + return this.idStyleMap.get(id) || null; + } + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; + } + generateId() { + return this.id++; + } + } + function getShadowHost(n) { + var _a, _b; + let shadowHost = null; + if (((_b = (_a = n.getRootNode) === null || _a === void 0 ? void 0 : _a.call(n)) === null || _b === void 0 ? void 0 : _b.nodeType) === Node.DOCUMENT_FRAGMENT_NODE && + n.getRootNode().host) + shadowHost = n.getRootNode().host; + return shadowHost; + } + function getRootShadowHost(n) { + let rootShadowHost = n; + let shadowHost; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; + return rootShadowHost; + } + function shadowHostInDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + const shadowHost = getRootShadowHost(n); + return doc.contains(shadowHost); + } + function inDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + return doc.contains(n) || shadowHostInDom(n); + } + + var EventType$1 = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType$1 || {}); + var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource$1 || {}); + var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; + })(MouseInteractions || {}); + var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; + })(PointerTypes || {}); + var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; + })(CanvasContext || {}); + + function isNodeInLinkedList(n) { + return '__ln' in n; + } + class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + this.tail = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + if (node.next === null) { + this.tail = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + else { + this.tail = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } + } + const moveKey = (id, parentId) => `${id}@${parentId}`; + class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addedIds = new Set(); + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + if (!n.parentNode || + !inDom(n) || + n.parentNode.tagName === 'TEXTAREA') { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + let tailNode = addList.tail; + while (tailNode) { + const _node = tailNode; + tailNode = tailNode.previous; + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (nextId === -1) + continue; + else if (parentId !== -1) { + node = _node; + break; + } + else { + const unhandledNode = _node.value; + if (unhandledNode.parentNode && + unhandledNode.parentNode.nodeType === + Node.DOCUMENT_FRAGMENT_NODE) { + const shadowHost = unhandledNode.parentNode + .host; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => { + const n = text.node; + if (n.parentNode && + n.parentNode.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(n.parentNode); + } + return { + id: this.mirror.getId(n), + value: text.value, + }; + }) + .filter((text) => !addedIds.has(text.id)) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => { + const { attributes } = attribute; + if (typeof attributes.style === 'string') { + const diffAsStr = JSON.stringify(attribute.styleDiff); + const unchangedAsStr = JSON.stringify(attribute._unchangedStyles); + if (diffAsStr.length < attributes.style.length) { + if ((diffAsStr + unchangedAsStr).split('var(').length === + attributes.style.split('var(').length) { + attributes.style = attribute.styleDiff; + } + } + } + return { + id: this.mirror.getId(attribute.node), + attributes: attributes, + }; + }) + .filter((attribute) => !addedIds.has(attribute.id)) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.genTextAreaValueMutation = (textarea) => { + let item = this.attributeMap.get(textarea); + if (!item) { + item = { + node: textarea, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(textarea, item); + } + item.attributes.value = Array.from(textarea.childNodes, (cn) => cn.textContent || '').join(''); + }; + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, false) && + value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, true) && value + ? this.maskTextFn + ? this.maskTextFn(value, closestElementOfNode(m.target)) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let attributeName = m.attributeName; + let value = m.target.getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ + element: target, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type, + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + value === m.oldValue) { + return; + } + let item = this.attributeMap.get(m.target); + if (target.tagName === 'IFRAME' && + attributeName === 'src' && + !this.keepIframeSrcFn(value)) { + if (!target.contentDocument) { + attributeName = 'rr_src'; + } + else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(m.target, item); + } + if (attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (!ignoreAttribute(target.tagName, attributeName)) { + item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value); + if (attributeName === 'style') { + if (!this.unattachedDoc) { + try { + this.unattachedDoc = + document.implementation.createHTMLDocument(); + } + catch (e) { + this.unattachedDoc = this.doc; + } + } + const old = this.unattachedDoc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + item.styleDiff[pname] = newValue; + } + else { + item.styleDiff[pname] = [newValue, newPriority]; + } + } + else { + item._unchangedStyles[pname] = [newValue, newPriority]; + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + item.styleDiff[pname] = false; + } + } + } + } + break; + } + case 'childList': { + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; + if (m.target.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(m.target); + return; + } + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (this.processedNodeManager.inOtherBuffer(n, this)) + return; + if (this.addedSet.has(n) || this.movedSet.has(n)) + return; + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); + } + } + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'dataURLOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'processedNodeManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } + } + function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); + } + function isParentRemoved(removes, n, mirror) { + if (removes.length === 0) + return false; + return _isParentRemoved(removes, n, mirror); + } + function _isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); + } + function isAncestorInSet(set, n) { + if (set.size === 0) + return false; + return _isAncestorInSet(set, n); + } + function _isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); + } + + let errorHandler; + function registerErrorHandler(handler) { + errorHandler = handler; + } + function unregisterErrorHandler() { + errorHandler = undefined; + } + const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { + try { + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return; + } + throw error; + } + }); + return rrwebWrapped; + }; + + const mutationBuffers = []; + function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { + } + return event && event.target; + } + function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer))); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; + } + function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { + }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle(callbackWrapper((source) => { + const totalOffset = Date.now() - timeBaseline; + mousemoveCb(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }), callbackThreshold); + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = legacy_isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = nowTimestamp(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: nowTimestamp() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource$1.Drag + : evt instanceof MouseEvent + ? IncrementalSource$1.MouseMove + : IncrementalSource$1.TouchMove); + }), threshold, { + trailing: false, + })); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { + }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + let currentPointerType = null; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, true)) { + return; + } + let pointerType = null; + let thisEventKey = eventKey; + if ('pointerType' in event) { + switch (event.pointerType) { + case 'mouse': + pointerType = PointerTypes.Mouse; + break; + case 'touch': + pointerType = PointerTypes.Touch; + break; + case 'pen': + pointerType = PointerTypes.Pen; + break; + } + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + thisEventKey = 'TouchStart'; + } + else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) { + thisEventKey = 'TouchEnd'; + } + } + else if (pointerType === PointerTypes.Pen) ; + } + else if (legacy_isTouchEvent(event)) { + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + if ((thisEventKey.startsWith('Touch') && + pointerType === PointerTypes.Touch) || + (thisEventKey.startsWith('Mouse') && + pointerType === PointerTypes.Mouse)) { + pointerType = null; + } + } + else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; + } + const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)(Object.assign({ type: MouseInteractions[thisEventKey], id, x: clientX, y: clientY }, (pointerType !== null && { pointerType }))); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + let eventName = toLowerCase(eventKey); + const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + return; + } + } + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }) { + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); + } + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }), sampling.scroll || 100)); + return on('scroll', updatePosition, doc); + } + function initViewportResizeObserver({ viewportResizeCb }, { win }) { + let lastH = -1; + let lastW = -1; + const updateDimension = callbackWrapper(throttle(callbackWrapper(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }), 200)); + return on('resize', updateDimension, win); + } + const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; + const lastInputValueMap = new WeakMap(); + function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + if (target.classList.contains(ignoreClass) || + (ignoreSelector && target.matches(ignoreSelector))) { + return; + } + let text = target.value; + let isChecked = false; + const type = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + else if (maskInputOptions[tagName.toLowerCase()] || + maskInputOptions[type]) { + text = maskInputValue({ + element: target, + maskInputOptions, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, userTriggeredOnInput + ? { text, isChecked, userTriggered } + : { text, isChecked }); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = el.value; + cbWithDedup(el, userTriggeredOnInput + ? { text, isChecked: !isChecked, userTriggered: false } + : { text, isChecked: !isChecked }); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + callbackWrapper(inputCb)(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const currentWindow = doc.defaultView; + if (!currentWindow) { + return () => { + handlers.forEach((h) => h()); + }; + } + const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [currentWindow.HTMLInputElement.prototype, 'value'], + [currentWindow.HTMLInputElement.prototype, 'checked'], + [currentWindow.HTMLSelectElement.prototype, 'value'], + [currentWindow.HTMLTextAreaElement.prototype, 'value'], + [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'], + [currentWindow.HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ + target: this, + isTrusted: false, + }); + }, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); + } + function getIdAndStyleId(sheet, mirror, styleMirror) { + let id, styleId; + if (!sheet) + return {}; + if (sheet.ownerNode) + id = mirror.getId(sheet.ownerNode); + else + styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; + } + function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { + }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + let replace; + if (win.CSSStyleSheet.prototype.replace) { + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = new Proxy(replace, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + let replaceSync; + if (win.CSSStyleSheet.prototype.replaceSync) { + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); + } + function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) { + var _a, _b, _c; + let hostId = null; + if (host.nodeName === '#document') + hostId = mirror.getId(host); + else + hostId = mirror.getId(host.host); + const patchTarget = host.nodeName === '#document' + ? (_a = host.defaultView) === null || _a === void 0 ? void 0 : _a.Document + : (_c = (_b = host.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.ShadowRoot; + const originalPropertyDescriptor = (patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype) + ? Object.getOwnPropertyDescriptor(patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype, 'adoptedStyleSheets') + : undefined; + if (hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor) + return () => { + }; + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get() { + var _a; + return (_a = originalPropertyDescriptor.get) === null || _a === void 0 ? void 0 : _a.call(this); + }, + set(sheets) { + var _a; + const result = (_a = originalPropertyDescriptor.set) === null || _a === void 0 ? void 0 : _a.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } + catch (e) { + } + } + return result; + }, + }); + return callbackWrapper(() => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get: originalPropertyDescriptor.get, + set: originalPropertyDescriptor.set, + }); + }); + } + function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property, value, priority] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return setProperty.apply(thisArg, [property, value, priority]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return removeProperty.apply(thisArg, [property]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); + } + function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, mirror, sampling, doc, }) { + const handler = callbackWrapper((type) => throttle(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const { currentTime, volume, muted, playbackRate, loop } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + playbackRate, + loop, + }); + }), sampling.media || 500)); + const handlers = [ + on('play', handler(0), doc), + on('pause', handler(1), doc), + on('seeked', handler(2), doc), + on('volumechange', handler(3), doc), + on('ratechange', handler(4), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { + }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(callbackWrapper(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }), 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initSelectionObserver(param) { + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; + let collapsed = true; + const updateSelection = callbackWrapper(() => { + const selection = doc.getSelection(); + if (!selection || (collapsed && (selection === null || selection === void 0 ? void 0 : selection.isCollapsed))) + return; + collapsed = selection.isCollapsed || false; + const ranges = []; + const count = selection.rangeCount || 0; + for (let i = 0; i < count; i++) { + const range = selection.getRangeAt(i); + const { startContainer, startOffset, endContainer, endOffset } = range; + const blocked = isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); + if (blocked) + continue; + ranges.push({ + start: mirror.getId(startContainer), + startOffset, + end: mirror.getId(endContainer), + endOffset, + }); + } + selectionCb({ ranges }); + }); + updateSelection(); + return on('selectionchange', updateSelection); + } + function initCustomElementObserver({ doc, customElementCb, }) { + const win = doc.defaultView; + if (!win || !win.customElements) + return () => { }; + const restoreHandler = patch(win.customElements, 'define', function (original) { + return function (name, constructor, options) { + try { + customElementCb({ + define: { + name, + }, + }); + } + catch (e) { + console.warn(`Custom element callback failed for ${name}`); + } + return original.apply(this, [name, constructor, options]); + }; + }); + return restoreHandler; + } + function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, selectionCb, customElementCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; + o.selectionCb = (...p) => { + if (hooks.selection) { + hooks.selection(...p); + } + selectionCb(...p); + }; + o.customElementCb = (...c) => { + if (hooks.customElement) { + hooks.customElement(...c); + } + customElementCb(...c); + }; + } + function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { + }; + } + mergeHooks(o, hooks); + let mutationObserver; + if (o.recordDOM) { + mutationObserver = initMutationObserver(o, o.doc); + } + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o, { + win: currentWindow, + }); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + let styleSheetObserver = () => { }; + let adoptedStyleSheetObserver = () => { }; + let styleDeclarationObserver = () => { }; + let fontObserver = () => { }; + if (o.recordDOM) { + styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); + styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + if (o.collectFonts) { + fontObserver = initFontObserver(o); + } + } + const selectionObserver = initSelectionObserver(o); + const customElementObserver = initCustomElementObserver(o); + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver === null || mutationObserver === void 0 ? void 0 : mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + adoptedStyleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + selectionObserver(); + customElementObserver(); + pluginHandlers.forEach((h) => h()); + }); + } + function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; + } + function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); + } + + class CrossOriginIframeMirror { + constructor(generateIdFn) { + this.generateIdFn = generateIdFn; + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + } + getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) { + const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe); + let id = idToRemoteIdMap.get(remoteId); + if (!id) { + id = this.generateIdFn(); + idToRemoteIdMap.set(remoteId, id); + remoteIdToIdMap.set(id, remoteId); + } + return id; + } + getIds(iframe, remoteId) { + const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + } + getRemoteId(iframe, id, map) { + const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe); + if (typeof id !== 'number') + return id; + const remoteId = remoteIdToIdMap.get(id); + if (!remoteId) + return -1; + return remoteId; + } + getRemoteIds(iframe, ids) { + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + } + reset(iframe) { + if (!iframe) { + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + return; + } + this.iframeIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToIdMap.delete(iframe); + } + getIdToRemoteIdMap(iframe) { + let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); + if (!idToRemoteIdMap) { + idToRemoteIdMap = new Map(); + this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + } + return idToRemoteIdMap; + } + getRemoteIdToIdMap(iframe) { + let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); + if (!remoteIdToIdMap) { + remoteIdToIdMap = new Map(); + this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + } + return remoteIdToIdMap; + } + } + + class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.crossOriginIframeMap = new WeakMap(); + this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + this.crossOriginIframeRootIdMap = new WeakMap(); + this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; + this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)); + this.mirror = options.mirror; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + if (iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0) + this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument)); + } + handleMessage(message) { + const crossOriginMessageEvent = message; + if (crossOriginMessageEvent.data.type !== 'rrweb' || + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin) + return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) + return; + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) + return; + const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event); + if (transformedEvent) + this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout); + } + transformCrossOriginEvent(iframeEl, e) { + var _a; + switch (e.type) { + case EventType$1.FullSnapshot: { + this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); + this.replaceIdOnNode(e.data.node, iframeEl); + const rootId = e.data.node.id; + this.crossOriginIframeRootIdMap.set(iframeEl, rootId); + this.patchRootIdOnNode(e.data.node, rootId); + return { + timestamp: e.timestamp, + type: EventType$1.IncrementalSnapshot, + data: { + source: IncrementalSource$1.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + case EventType$1.Meta: + case EventType$1.Load: + case EventType$1.DomContentLoaded: { + return false; + } + case EventType$1.Plugin: { + return e; + } + case EventType$1.Custom: { + this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); + return e; + } + case EventType$1.IncrementalSnapshot: { + switch (e.data.source) { + case IncrementalSource$1.Mutation: { + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, [ + 'parentId', + 'nextId', + 'previousId', + ]); + this.replaceIdOnNode(n.node, iframeEl); + const rootId = this.crossOriginIframeRootIdMap.get(iframeEl); + rootId && this.patchRootIdOnNode(n.node, rootId); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.Drag: + case IncrementalSource$1.TouchMove: + case IncrementalSource$1.MouseMove: { + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.ViewportResize: { + return false; + } + case IncrementalSource$1.MediaInteraction: + case IncrementalSource$1.MouseInteraction: + case IncrementalSource$1.Scroll: + case IncrementalSource$1.CanvasMutation: + case IncrementalSource$1.Input: { + this.replaceIds(e.data, iframeEl, ['id']); + return e; + } + case IncrementalSource$1.StyleSheetRule: + case IncrementalSource$1.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleId']); + return e; + } + case IncrementalSource$1.Font: { + return e; + } + case IncrementalSource$1.Selection: { + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); + return e; + } + case IncrementalSource$1.AdoptedStyleSheet: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); + return e; + } + } + } + } + return false; + } + replace(iframeMirror, obj, iframeEl, keys) { + for (const key of keys) { + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') + continue; + if (Array.isArray(obj[key])) { + obj[key] = iframeMirror.getIds(iframeEl, obj[key]); + } + else { + obj[key] = iframeMirror.getId(iframeEl, obj[key]); + } + } + return obj; + } + replaceIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); + } + replaceStyleIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); + } + replaceIdOnNode(node, iframeEl) { + this.replaceIds(node, iframeEl, ['id', 'rootId']); + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } + patchRootIdOnNode(node, rootId) { + if (node.type !== NodeType.Document && !node.rootId) + node.rootId = rootId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.patchRootIdOnNode(child, rootId); + }); + } + } + } + + class ShadowDomManager { + constructor(options) { + this.shadowDoms = new WeakSet(); + this.restoreHandlers = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + this.init(); + } + init() { + this.reset(); + this.patchAttachShadow(Element, document); + } + addShadowRoot(shadowRoot, doc) { + if (!isNativeShadowDom(shadowRoot)) + return; + if (this.shadowDoms.has(shadowRoot)) + return; + this.shadowDoms.add(shadowRoot); + const observer = initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + this.restoreHandlers.push(() => observer.disconnect()); + this.restoreHandlers.push(initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror }))); + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0) + this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host)); + this.restoreHandlers.push(initAdoptedStyleSheetObserver({ + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, shadowRoot)); + }, 0); + } + observeAttachShadow(iframeElement) { + if (!iframeElement.contentWindow || !iframeElement.contentDocument) + return; + this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument); + } + patchAttachShadow(element, doc) { + const manager = this; + this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) { + return function (option) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot && inDom(this)) + manager.addShadowRoot(this.shadowRoot, doc); + return shadowRoot; + }; + })); + } + reset() { + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } + catch (e) { + } + }); + this.restoreHandlers = []; + this.shadowDoms = new WeakSet(); + } + } + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; + } + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const canvasVarMap = new Map(); + function variableListFor(ctx, ctor) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); + } + const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; + }; + function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + const src = value.toDataURL(); + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; + } + const serializeArgs = (args, win, ctx) => { + return args.map((arg) => serializeArg(arg, win, ctx)); + }; + const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); + }; + + function initCanvas2DMutationObserver(cb, win, blockClass, blockSelector) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { + setTimeout(() => { + const recordArgs = serializeArgs(args, win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function getNormalizedContextName(contextType) { + return contextType === 'experimental-webgl' ? 'webgl' : contextType; + } + function initCanvasContextObserver(win, blockClass, blockSelector, setPreserveDrawingBufferToTrue) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, true)) { + const ctxName = getNormalizedContextName(contextType); + if (!('__context' in this)) + this.__context = ctxName; + if (setPreserveDrawingBufferToTrue && + ['webgl', 'webgl2'].includes(ctxName)) { + if (args[0] && typeof args[0] === 'object') { + const contextAttributes = args[0]; + if (!contextAttributes.preserveDrawingBuffer) { + contextAttributes.preserveDrawingBuffer = true; + } + } + else { + args.splice(0, 1, { + preserveDrawingBuffer: true, + }); + } + } + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function patchGLPrototype(prototype, type, cb, blockClass, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + if ([ + 'isContextLost', + 'canvas', + 'drawingBufferWidth', + 'drawingBufferHeight', + ].includes(prop)) { + continue; + } + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, this); + if ('tagName' in this.canvas && + !isBlocked(this.canvas, blockClass, blockSelector, true)) { + const recordArgs = serializeArgs(args, win, this); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; + } + function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function funcToSource(fn, sourcemapArg) { + var sourcemap = sourcemapArg === undefined ? null : sourcemapArg; + var source = fn.toString(); + var lines = source.split('\n'); + lines.pop(); + lines.shift(); + var blankPrefixLength = lines[0].search(/\S/); + var regex = /(['"])__worker_loader_strict__(['"])/g; + for (var i = 0, n = lines.length; i < n; ++i) { + lines[i] = lines[i].substring(blankPrefixLength).replace(regex, '$1use strict$2') + '\n'; + } + if (sourcemap) { + lines.push('\/\/# sourceMappingURL=' + sourcemap + '\n'); + } + return lines; + } + + function createURL(fn, sourcemapArg) { + var lines = funcToSource(fn, sourcemapArg); + var blob = new Blob(lines, { type: 'application/javascript' }); + return URL.createObjectURL(blob); + } + + function createInlineWorkerFactory(fn, sourcemapArg) { + var url; + return function WorkerFactory(options) { + url = url || createURL(fn, sourcemapArg); + return new Worker(url, options); + }; + } + + var WorkerFactory = createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () { + (function () { + '__worker_loader_strict__'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const lastBlobMap = new Map(); + const transparentBlobMap = new Map(); + function getTransparentBlobFor(width, height, dataURLOptions) { + return __awaiter(this, void 0, void 0, function* () { + const id = `${width}-${height}`; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) + return transparentBlobMap.get(id); + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + transparentBlobMap.set(id, base64); + return base64; + } + else { + return ''; + } + }); + } + const worker = self; + worker.onmessage = function (e) { + return __awaiter(this, void 0, void 0, function* () { + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor(width, height, dataURLOptions); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const type = blob.type; + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + if (!lastBlobMap.has(id) && (yield transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + if (lastBlobMap.get(id) === base64) + return worker.postMessage({ id }); + worker.postMessage({ + id, + type, + base64, + width, + height, + }); + lastBlobMap.set(id, base64); + } + else { + return worker.postMessage({ id: e.data.id }); + } + }); + }; + + })(); + }, null); + + class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = (target, mutation) => { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas, dataURLOptions, } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass, blockSelector); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { + dataURLOptions, + }); + } + initCanvasFPSObserver(fps, win, blockClass, blockSelector, options) { + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, true); + const snapshotInProgressMap = new Map(); + const worker = new WorkerFactory(); + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + if (!('base64' in e.data)) + return; + const { base64, type, width, height } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', + args: [0, 0, width, height], + }, + { + property: 'drawImage', + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + }, + 0, + 0, + ], + }, + ], + }); + }; + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId; + const getCanvas = () => { + const matchedCanvas = []; + win.document.querySelectorAll('canvas').forEach((canvas) => { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { + matchedCanvas.push(canvas); + } + }); + return matchedCanvas; + }; + const takeCanvasSnapshots = (timestamp) => { + if (lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + getCanvas() + .forEach((canvas) => __awaiter(this, void 0, void 0, function* () { + var _a; + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) + return; + if (canvas.width === 0 || canvas.height === 0) + return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes(canvas.__context)) { + const context = canvas.getContext(canvas.__context); + if (((_a = context === null || context === void 0 ? void 0 : context.getContextAttributes()) === null || _a === void 0 ? void 0 : _a.preserveDrawingBuffer) === false) { + context.clear(context.COLOR_BUFFER_BIT); + } + } + const bitmap = yield createImageBitmap(canvas); + worker.postMessage({ + id, + bitmap, + width: canvas.width, + height: canvas.height, + dataURLOptions: options.dataURLOptions, + }, [bitmap]); + })); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + rafId = requestAnimationFrame(takeCanvasSnapshots); + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + initCanvasMutationObserver(win, blockClass, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, false); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } + } + + class StylesheetManager { + constructor(options) { + this.trackedLinkElements = new WeakSet(); + this.styleMirror = new StyleSheetMirror(); + this.mutationCb = options.mutationCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; + } + attachLinkElement(linkEl, childSn) { + if ('_cssText' in childSn.attributes) + this.mutationCb({ + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: childSn.id, + attributes: childSn + .attributes, + }, + ], + }); + this.trackLinkElement(linkEl); + } + trackLinkElement(linkEl) { + if (this.trackedLinkElements.has(linkEl)) + return; + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + adoptStyleSheets(sheets, hostId) { + if (sheets.length === 0) + return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [], + }; + const styles = []; + for (const sheet of sheets) { + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + styles.push({ + styleId, + rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ + rule: stringifyRule(r), + index, + })), + }); + } + else + styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); + } + if (styles.length > 0) + adoptedStyleSheetData.styles = styles; + this.adoptedStyleSheetCb(adoptedStyleSheetData); + } + reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + trackStylesheetInLinkElement(linkEl) { + } + } + + class ProcessedNodeManager { + constructor() { + this.nodeMap = new WeakMap(); + this.loop = true; + this.periodicallyClear(); + } + periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + if (this.loop) + this.periodicallyClear(); + }); + } + inOtherBuffer(node, thisBuffer) { + const buffers = this.nodeMap.get(node); + return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + } + add(node, buffer) { + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + } + clear() { + this.nodeMap = new WeakMap(); + } + destroy() { + this.loop = false; + } + } + + function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: nowTimestamp() }); + } + let wrappedEmit; + let takeFullSnapshot; + let canvasManager; + let recording = false; + const mirror = createMirror(); + function record(options = {}) { + const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordDOM = true, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded' + ? options.recordAfter + : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; + registerErrorHandler(errorHandler); + const inEmittingFrame = recordCrossOriginIframes + ? window.parent === window + : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + if (window.parent.document) { + passEmitsToParent = false; + } + } + catch (e) { + passEmitsToParent = true; + } + } + if (inEmittingFrame && !emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + mirror.reset(); + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn && + !passEmitsToParent) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType$1.FullSnapshot && + !(e.type === EventType$1.IncrementalSnapshot && + e.data.source === IncrementalSource$1.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + if (inEmittingFrame) { + emit === null || emit === void 0 ? void 0 : emit(eventProcessor(e), isCheckout); + } + else if (passEmitsToParent) { + const message = { + type: 'rrweb', + event: eventProcessor(e), + origin: window.location.origin, + isCheckout, + }; + window.parent.postMessage(message, '*'); + } + if (e.type === EventType$1.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType$1.IncrementalSnapshot) { + if (e.data.source === IncrementalSource$1.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p), + })); + const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a), + })); + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, + }); + const iframeManager = new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + }); + } + const processedNodeManager = new ProcessedNodeManager(); + canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + if (!recordDOM) { + return; + } + wrappedEmit(wrapEvent({ + type: EventType$1.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + stylesheetManager.reset(); + shadowDomManager.init(); + mutationBuffers.forEach((buf) => buf.lock()); + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + dataURLOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.trackLinkElement(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachLinkElement(linkEl, childSn); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.FullSnapshot, + data: { + node, + initialOffset: getWindowScroll(window), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.unlock()); + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document)); + }; + try { + const handlers = []; + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Font }, p), + })), + selectionCb: (p) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Selection }, p), + })); + }, + customElementCb: (c) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CustomElement }, c), + })); + }, + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordDOM, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + dataURLOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + processedNodeManager, + canvasManager, + ignoreCSSAttributes, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType$1.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + recording = true; + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.DomContentLoaded, + data: {}, + })); + if (recordAfter === 'DOMContentLoaded') + init(); + })); + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.Load, + data: {}, + })); + if (recordAfter === 'load') + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + processedNodeManager.destroy(); + recording = false; + unregisterErrorHandler(); + }; + } + catch (error) { + console.warn(error); + } + } + record.addCustomEvent = (tag, payload) => { + if (!recording) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.Custom, + data: { + tag, + payload, + }, + })); + }; + record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); + }; + record.takeFullSnapshot = (isCheckout) => { + if (!recording) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); + }; + record.mirror = mirror; + + var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType || {}); + var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource || {}); + var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -62,7 +4569,7 @@ define((function () { 'use strict'; }; // Console override - var console = { + var console$1 = { /** @type {function(...*)} */ log: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { @@ -119,14 +4626,14 @@ define((function () { 'use strict'; var log_func_with_prefix = function(func, prefix) { return function() { arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); + return func.apply(console$1, arguments); }; }; var console_with_prefix = function(prefix) { return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) + log: log_func_with_prefix(console$1.log, prefix), + error: log_func_with_prefix(console$1.error, prefix), + critical: log_func_with_prefix(console$1.critical, prefix) }; }; @@ -979,7 +5486,7 @@ define((function () { 'use strict'; try { result = decodeURIComponent(result); } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); + console$1.error('Skipping decoding for malformed query param: ' + result); } return result.replace(/\+/g, ' '); } @@ -1106,13 +5613,13 @@ define((function () { 'use strict'; is_supported: function(force_check) { var supported = localStorageSupported(null, force_check); if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); + console$1.error('localStorage unsupported; falling back to cookie store'); } return supported; }, error: function(msg) { - console.error('localStorage error: ' + msg); + console$1.error('localStorage error: ' + msg); }, get: function(name) { @@ -1167,7 +5674,7 @@ define((function () { 'use strict'; */ var register_event = function(element, type, handler, oldSchool, useCapture) { if (!element) { - console.error('No valid element provided to register_event'); + console$1.error('No valid element provided to register_event'); return; } @@ -1729,158 +6236,306 @@ define((function () { 'use strict'; _['info']['browserVersion'] = _.info.browserVersion; _['info']['properties'] = _.info.properties; - /* eslint camelcase: "off" */ - /** - * DomTracker Object - * @constructor + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. */ - var DomTracker = function() {}; - - - // interface - DomTracker.prototype.create_properties = function() {}; - DomTracker.prototype.event_handler = function() {}; - DomTracker.prototype.after_track_handler = function() {}; - - DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; - }; /** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ - DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - that.event_handler(e, this, options); + /** Public **/ - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); + /** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function optIn(token, options) { + _optInOut(true, token, options); + } - return true; - }; + /** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ + function optOut(token, options) { + _optInOut(false, token, options); + } /** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type */ - DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; + function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; + } - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; + /** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ + function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console$1.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console$1.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; + } - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } + /** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); + } - that.after_track_handler(props, options, timeout_occured); - }; - }; + /** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); + } - DomTracker.prototype.create_properties = function(properties, element) { - var props; + /** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); + } - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } + /** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); + } - return props; - }; + /** Private **/ /** - * LinkTracker Object - * @constructor - * @extends DomTracker + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage */ - var LinkTracker = function() { - this.override_event = 'click'; - }; - _.inherit(LinkTracker, DomTracker); + function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; + } - LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); + /** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ + function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; + } - if (element.href) { props['url'] = element.href; } + /** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ + function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); + } - return props; - }; + /** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ + function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; - LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); - if (!options.new_tab) { - evt.preventDefault(); + return hasDntOn; + } + + /** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console$1.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; } - }; - LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } + options = options || {}; - setTimeout(function() { - window.location = options.href; - }, 0); - }; + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } + } /** - * FormTracker Object - * @constructor - * @extends DomTracker + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out */ - var FormTracker = function() { - this.override_event = 'submit'; - }; - _.inherit(FormTracker, DomTracker); + function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; - FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); - }; + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests - FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); - }; + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console$1.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } - var logger$2 = console_with_prefix('lock'); + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; + } + + var logger$3 = console_with_prefix('lock'); /** * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser @@ -1937,7 +6592,7 @@ define((function () { 'use strict'; var delay = function(cb) { if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); storage.removeItem(keyZ); storage.removeItem(keyY); loop(); @@ -2026,7 +6681,7 @@ define((function () { 'use strict'; } }; - var logger$1 = console_with_prefix('batch'); + var logger$2 = console_with_prefix('batch'); /** * RequestQueue: queue for batching API requests with localStorage backup for retries. @@ -2048,9 +6703,10 @@ define((function () { 'use strict'; options = options || {}; this.storageKey = storageKey; this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2075,29 +6731,36 @@ define((function () { 'use strict'; 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2108,7 +6771,7 @@ define((function () { 'use strict'; */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2157,61 +6820,67 @@ define((function () { 'use strict'; _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2239,25 +6908,32 @@ define((function () { 'use strict'; */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } + if (!this.usePersistence) { if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2300,13 +6976,16 @@ define((function () { 'use strict'; */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - var logger = console_with_prefix('batch'); + var logger$1 = console_with_prefix('batch'); /** * RequestBatcher: manages the queueing, flushing, retry etc of requests of one @@ -2318,7 +6997,8 @@ define((function () { 'use strict'; this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2335,6 +7015,11 @@ define((function () { 'use strict'; // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2410,7 +7095,7 @@ define((function () { 'use strict'; try { if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); + logger$1.log('Flush: Request already in progress'); return; } @@ -2419,6 +7104,9 @@ define((function () { 'use strict'; var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2486,22 +7174,17 @@ define((function () { 'use strict'; this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2525,7 +7208,11 @@ define((function () { 'use strict'; _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2571,9 +7258,8 @@ define((function () { 'use strict'; if (options.unloading) { requestOptions.transport = 'sendBeacon'; } - logger.log('MIXPANEL REQUEST:', dataForRequest); + logger$1.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2584,7 +7270,7 @@ define((function () { 'use strict'; * Log error to global logger and optional user-defined logger. */ RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); + logger$1.error.apply(logger$1.error, arguments); if (this.errorReporter) { try { if (!(err instanceof Error)) { @@ -2592,309 +7278,402 @@ define((function () { 'use strict'; } this.errorReporter(msg, err); } catch(err) { - logger.error(err); + logger$1.error(err); } } }; - /** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - - /** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ + var logger = console_with_prefix('recorder'); + var CompressionStream = win['CompressionStream']; - /** Public **/ + var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true + }; - var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, + ]); - /** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ - function optIn(token, options) { - _optInOut(true, token, options); + function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); } - /** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ - function optOut(token, options) { - _optInOut(false, token, options); - } + var MixpanelRecorder = function(mixpanelInstance) { + this._mixpanel = mixpanelInstance; - /** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ - function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; - } + // internal rrweb stopRecording function + this._stopRecording = null; - /** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ - function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; + this.recEvents = []; + this.seqNo = 0; + this.replayId = null; + this.replayStartTime = null; + this.sendBatchId = null; + + this.idleTimeoutId = null; + this.maxTimeoutId = null; + + this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); + }; + + + MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); + }; + + // eslint-disable-next-line camelcase + MixpanelRecorder.prototype.get_config = function(configVar) { + return this._mixpanel.get_config(configVar); + }; + + MixpanelRecorder.prototype.startRecording = function () { + if (this._stopRecording !== null) { + logger.log('Recording already in progress, skipping startRecording.'); + return; } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + + this.recordMaxMs = this.get_config('record_max_ms'); + if (this.recordMaxMs > MAX_RECORDING_MS) { + this.recordMaxMs = MAX_RECORDING_MS; + logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.'); } - return optedOut; - } - /** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ - function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); - } + this.recEvents = []; + this.seqNo = 0; + this.replayStartTime = null; - /** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ - function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + this.replayId = _.UUID(); + + this.batcher.start(); + + var resetIdleTimeout = _.bind(function () { + clearTimeout(this.idleTimeoutId); + this.idleTimeoutId = setTimeout(_.bind(function () { + logger.log('Idle timeout reached, restarting recording.'); + this.resetRecording(); + }, this), this.get_config('record_idle_timeout_ms')); + }, this); + + this._stopRecording = record({ + 'emit': _.bind(function (ev) { + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } + }, this), + 'blockClass': this.get_config('record_block_class'), + 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, + 'maskTextClass': this.get_config('record_mask_text_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); - } + + resetIdleTimeout(); + + this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); + }; + + MixpanelRecorder.prototype.resetRecording = function () { + this.stopRecording(); + this.startRecording(); + }; + + MixpanelRecorder.prototype.stopRecording = function () { + if (this._stopRecording !== null) { + this._stopRecording(); + this._stopRecording = null; + } + + this.batcher.flush(); // flush any remaining events + this.replayId = null; + + clearTimeout(this.idleTimeoutId); + clearTimeout(this.maxTimeoutId); + }; /** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out + * Flushes the current batch of events to the server, but passes an opt-out callback to make sure + * we stop recording and dump any queued events if the user has opted out. */ - function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); + }; + + MixpanelRecorder.prototype._onOptOut = function (code) { + // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out + if (code === 0) { + this.recEvents = []; + this.stopRecording(); + } + }; + + MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/octet-stream' + }, + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); - } + }; - /** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ - function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); - } + MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + + var reqParams = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + var eventsJson = _.JSONEncode(data); + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqParams['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqParams['$user_id'] = userId; + } + + if (CompressionStream) { + var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); + var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); + new Response(gzipStream) + .blob() + .then(_.bind(function(compressedBlob) { + reqParams['format'] = 'gzip'; + this._sendRequest(reqParams, compressedBlob, callback); + }, this)); + } else { + reqParams['format'] = 'body'; + this._sendRequest(reqParams, eventsJson, callback); + } + } + }); + + + MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } + }; - /** Private **/ - /** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ - function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; - } + win['__mp_recorder'] = MixpanelRecorder; - /** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ - function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; - } + /* eslint camelcase: "off" */ /** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type + * DomTracker Object + * @constructor */ - function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); - } + var DomTracker = function() {}; - /** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ - function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); + // interface + DomTracker.prototype.create_properties = function() {}; + DomTracker.prototype.event_handler = function() {}; + DomTracker.prototype.after_track_handler = function() {}; - return hasDntOn; - } + DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; + }; /** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback */ - function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); return; } - options = options || {}; + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); + that.event_handler(e, this, options); - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); }); - } - } + }, this); + + return true; + }; /** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured */ - function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; + DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; } - if (!optedOut) { - return method.apply(this, arguments); - } + that.after_track_handler(props, options, timeout_occured); + }; + }; - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } + DomTracker.prototype.create_properties = function(properties, element) { + var props; - return; - }; - } + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; + }; + + /** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ + var LinkTracker = function() { + this.override_event = 'click'; + }; + _.inherit(LinkTracker, DomTracker); + + LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; + }; + + LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } + }; + + LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); + }; + + /** + * FormTracker Object + * @constructor + * @extends DomTracker + */ + var FormTracker = function() { + this.override_event = 'submit'; + }; + _.inherit(FormTracker, DomTracker); + + FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); + }; + + FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); + }; /* eslint camelcase: "off" */ @@ -3316,7 +8095,7 @@ define((function () { 'use strict'; _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); return; } else { $add[k] = v; @@ -3441,7 +8220,7 @@ define((function () { 'use strict'; if (!_.isNumber(amount)) { amount = parseFloat(amount); if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } @@ -3478,7 +8257,7 @@ define((function () { 'use strict'; */ MixpanelPeople.prototype.delete_user = function() { if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); return; } var data = {'$delete': this._mixpanel.get_distinct_id()}; @@ -3552,7 +8331,7 @@ define((function () { 'use strict'; } else if (UNION_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); } else { - console.error('Invalid call to _enqueue():', data); + console$1.error('Invalid call to _enqueue():', data); } }; @@ -3700,7 +8479,7 @@ define((function () { 'use strict'; var storage_type = config['persistence']; if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); storage_type = config['persistence'] = 'cookie'; } @@ -4004,8 +8783,8 @@ define((function () { 'use strict'; this._pop_from_people_queue(UNSET_ACTION, q_data); } - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); this.save(); }; @@ -4050,7 +8829,7 @@ define((function () { 'use strict'; } else if (queue === UNION_ACTION) { return UNION_QUEUE_KEY; } else { - console.error('Invalid queue:', queue); + console$1.error('Invalid queue:', queue); } }; @@ -4107,6 +8886,12 @@ define((function () { 'use strict'; */ var init_type; // MODULE or SNIPPET loader + // allow bundlers to specify how extra code (recorder bundle) should be loaded + // eslint-disable-next-line no-unused-vars + var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); + }; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -4200,7 +8985,9 @@ define((function () { 'use strict'; 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4233,7 +9020,7 @@ define((function () { 'use strict'; instance = target; } else { if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); + console$1.error('You have already initialized ' + name); return; } instance = new MixpanelLib(); @@ -4361,9 +9148,9 @@ define((function () { 'use strict'; if (this._batch_requests) { if (!_.localStorage.is_supported(true) || !USE_XHR) { this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); + console$1.log('Clearing batch queue ' + batcher_config.queue_key); _.localStorage.remove(batcher_config.queue_key); }); } else { @@ -4426,7 +9213,7 @@ define((function () { 'use strict'; MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); + console$1.critical('Browser does not support MutationObserver; skipping session recording'); return; } @@ -4436,12 +9223,7 @@ define((function () { 'use strict'; }, this); if (_.isUndefined(win['__mp_recorder'])) { - var scriptEl = document$1.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document$1.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -4451,7 +9233,7 @@ define((function () { 'use strict'; if (this._recorder) { this._recorder['stopRecording'](); } else { - console.critical('Session recorder module not loaded'); + console$1.critical('Session recorder module not loaded'); } }; @@ -4740,7 +9522,8 @@ define((function () { 'use strict'; lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4840,6 +9623,7 @@ define((function () { 'use strict'; attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4851,8 +9635,8 @@ define((function () { 'use strict'; beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); @@ -4944,8 +9728,8 @@ define((function () { 'use strict'; truncated_data = this._run_hook('before_send_' + options.type, truncated_data); } if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); return this._send_request( endpoint, this._encode_data_for_request(truncated_data), @@ -6142,14 +10926,14 @@ define((function () { 'use strict'; }; MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); + console$1.error.apply(console$1.error, arguments); try { if (!err && !(msg instanceof Error)) { msg = new Error(msg); } this.get_config('error_reporter')(msg, err); } catch(err) { - console.error(err); + console$1.error(err); } }; @@ -6301,7 +11085,8 @@ define((function () { 'use strict'; _.register_event(win, 'load', dom_loaded_handler, true); }; - function init_as_module() { + function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); @@ -6312,9 +11097,16 @@ define((function () { 'use strict'; return mixpanel_master; } + // For loading separate bundles asynchronously via script tag + + // For builds that have everything in one bundle, no extra work. + function loadNoop (_src, onload) { + onload(); + } + /* eslint camelcase: "off" */ - var mixpanel = init_as_module(); + var mixpanel = init_as_module(loadNoop); return mixpanel; diff --git a/dist/mixpanel.cjs.js b/dist/mixpanel.cjs.js index 037d365e..6d9cc6dd 100644 --- a/dist/mixpanel.cjs.js +++ b/dist/mixpanel.cjs.js @@ -1,8 +1,4515 @@ 'use strict'; +var NodeType; +(function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; +})(NodeType || (NodeType = {})); + +function isElement(n) { + return n.nodeType === n.ELEMENT_NODE; +} +function isShadowRoot(n) { + const host = n === null || n === void 0 ? void 0 : n.host; + return Boolean((host === null || host === void 0 ? void 0 : host.shadowRoot) === n); +} +function isNativeShadowDom(shadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; +} +function fixBrowserCompatibilityIssuesInCSS(cssText) { + if (cssText.includes(' background-clip: text;') && + !cssText.includes(' -webkit-background-clip: text;')) { + cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;'); + } + return cssText; +} +function escapeImportStatement(rule) { + const { cssText } = rule; + if (cssText.split('"').length < 3) + return cssText; + const statement = ['@import', `url(${JSON.stringify(rule.href)})`]; + if (rule.layerName === '') { + statement.push(`layer`); + } + else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); + } + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); + } + if (rule.media.length) { + statement.push(rule.media.mediaText); + } + return statement.join(' ') + ';'; +} +function stringifyStylesheet(s) { + try { + const rules = s.rules || s.cssRules; + return rules + ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) + : null; + } + catch (error) { + return null; + } +} +function stringifyRule(rule) { + let importStringified; + if (isCSSImportRule(rule)) { + try { + importStringified = + stringifyStylesheet(rule.styleSheet) || + escapeImportStatement(rule); + } + catch (error) { + } + } + else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + return fixSafariColons(rule.cssText); + } + return importStringified || rule.cssText; +} +function fixSafariColons(cssStringified) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); +} +function isCSSImportRule(rule) { + return 'styleSheet' in rule; +} +function isCSSStyleRule(rule) { + return 'selectorText' in rule; +} +class Mirror { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} +function createMirror() { + return new Mirror(); +} +function maskInputValue({ element, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + const actualType = type && toLowerCase(type); + if (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType])) { + if (maskInputFn) { + text = maskInputFn(text, element); + } + else { + text = '*'.repeat(text.length); + } + } + return text; +} +function toLowerCase(str) { + return str.toLowerCase(); +} +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; +} +function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? + toLowerCase(type) + : null; +} +function extractFileExtension(path, baseURL) { + var _a; + let url; + try { + url = new URL(path, baseURL !== null && baseURL !== void 0 ? baseURL : window.location.href); + } + catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; + const match = url.pathname.match(regex); + return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null; +} + +let _id = 1; +const tagNameRegex = new RegExp('[^a-z0-9-_:]'); +const IGNORED_NODE = -2; +function genId() { + return _id++; +} +function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; +} +function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; +} +let canvasService; +let canvasCtx; +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; +const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; +const URL_WWW_MATCH = /^www\..*/i; +const DATA_URI = /^(data:)([^,]*),(.*)/i; +function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); +} +const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; +const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; +function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + const output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); +} +function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; +} +function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); +} +function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; +} +function transformAttribute(doc, tagName, name, value) { + if (!value) { + return value; + } + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + return value; +} +function ignoreAttribute(tagName, name, _value) { + return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; +} +function _isBlockedElement(element, blockClass, blockSelector) { + try { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = element.classList.length; eIndex--;) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + } + catch (e) { + } + return false; +} +function classMatchesRegex(node, regex, checkAncestors) { + if (!node) + return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + for (let eIndex = node.classList.length; eIndex--;) { + const className = node.classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); +} +function needMaskingText(node, maskTextClass, maskTextSelector, checkAncestors) { + try { + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + if (el === null) + return false; + if (typeof maskTextClass === 'string') { + if (checkAncestors) { + if (el.closest(`.${maskTextClass}`)) + return true; + } + else { + if (el.classList.contains(maskTextClass)) + return true; + } + } + else { + if (classMatchesRegex(el, maskTextClass, checkAncestors)) + return true; + } + if (maskTextSelector) { + if (checkAncestors) { + if (el.closest(maskTextSelector)) + return true; + } + else { + if (el.matches(maskTextSelector)) + return true; + } + } + } + catch (e) { + } + return false; +} +function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); + } + iframeEl.addEventListener('load', listener); +} +function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; + } + catch (error) { + return; + } + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); +} +function serializeNode(n, options) { + const { doc, mirror, blockClass, blockSelector, needsMask, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options; + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: n.compatMode, + }; + } + else { + return { + type: NodeType.Document, + childNodes: [], + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n, { + needsMask, + maskTextFn, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } +} +function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; +} +function serializeTextNode(n, options) { + var _a; + const { needsMask, maskTextFn, rootId } = options; + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStylesheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (!isStyle && !isScript && textContent && needsMask) { + textContent = maskTextFn + ? maskTextFn(textContent, n.parentElement) + : textContent.replace(/[\S]/g, '*'); + } + return { + type: NodeType.Text, + textContent: textContent || '', + isStyle, + rootId, + }; +} +function serializeElementNode(n, options) { + const { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions = {}, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, } = options; + const needBlock = _isBlockedElement(n, blockClass, blockSelector); + const tagName = getValidTagName(n); + let attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = stringifyStylesheet(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = n.value; + const checked = n.checked; + if (attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value) { + attributes.value = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } + else if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + image.removeEventListener('load', recordInlineImage); + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.addEventListener('load', recordInlineImage); + } + if (tagName === 'audio' || tagName === 'video') { + const mediaAttributes = attributes; + mediaAttributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + mediaAttributes.rr_mediaCurrentTime = n.currentTime; + mediaAttributes.rr_mediaPlaybackRate = n.playbackRate; + mediaAttributes.rr_mediaMuted = n.muted; + mediaAttributes.rr_mediaLoop = n.loop; + mediaAttributes.rr_mediaVolume = n.volume; + } + if (!newlyAddedElement) { + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + let isCustomElement; + try { + if (customElements.get(tagName)) + isCustomElement = true; + } + catch (e) { + } + return { + type: NodeType.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + isCustom: isCustomElement, + }; +} +function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } +} +function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + return true; + } + else if (sn.type === NodeType.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + extractFileExtension(sn.attributes.href) === 'js'))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; +} +function serializeNodeWithId(n, options) { + const { doc, mirror, blockClass, blockSelector, maskTextClass, maskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options; + let { needsMask } = options; + let { preserveWhiteSpace = true } = options; + if (!needsMask && + n.childNodes) { + const checkAncestors = needsMask === undefined; + needsMask = needMaskingText(n, maskTextClass, maskTextSelector, checkAncestors); + } + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if (mirror.hasNode(n)) { + id = mirror.getId(n); + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + mirror.add(n, serializedNode); + if (id === IGNORED_NODE) { + return null; + } + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + const shadowRoot = n.shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType.Document || + serializedNode.type === NodeType.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }; + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'textarea' && + serializedNode.attributes.value !== undefined) ; + else { + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css'))) { + onceStylesheetLoaded(n, () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedLinkNode) { + onStylesheetLoad(n, serializedLinkNode); + } + } + }, stylesheetLoadTimeout); + } + return serializedNode; +} +function snapshot(n, options) { + const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + }); +} + +function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); +} +const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; +let _mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, +}; +if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); +} +function throttle(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (...args) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; +} +function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); +} +function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { + }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { + }; + } +} +let nowTimestamp = Date.now; +if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) { + nowTimestamp = () => new Date().getTime(); +} +function getWindowScroll(win) { + var _a, _b, _c, _d, _e, _f; + const doc = win.document; + return { + left: doc.scrollingElement + ? doc.scrollingElement.scrollLeft + : win.pageXOffset !== undefined + ? win.pageXOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollLeft) || + ((_b = (_a = doc === null || doc === void 0 ? void 0 : doc.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || + ((_c = doc === null || doc === void 0 ? void 0 : doc.body) === null || _c === void 0 ? void 0 : _c.scrollLeft) || + 0, + top: doc.scrollingElement + ? doc.scrollingElement.scrollTop + : win.pageYOffset !== undefined + ? win.pageYOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollTop) || + ((_e = (_d = doc === null || doc === void 0 ? void 0 : doc.body) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.scrollTop) || + ((_f = doc === null || doc === void 0 ? void 0 : doc.body) === null || _f === void 0 ? void 0 : _f.scrollTop) || + 0, + }; +} +function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); +} +function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); +} +function closestElementOfNode(node) { + if (!node) { + return null; + } + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + return el; +} +function isBlocked(node, blockClass, blockSelector, checkAncestors) { + if (!node) { + return false; + } + const el = closestElementOfNode(node); + if (!el) { + return false; + } + try { + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) + return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) + return true; + } + else { + if (classMatchesRegex(el, blockClass, checkAncestors)) + return true; + } + } + catch (e) { + } + if (blockSelector) { + if (el.matches(blockSelector)) + return true; + if (checkAncestors && el.closest(blockSelector) !== null) + return true; + } + return false; +} +function isSerialized(n, mirror) { + return mirror.getId(n) !== -1; +} +function isIgnored(n, mirror) { + return mirror.getId(n) === IGNORED_NODE; +} +function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); +} +function legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); +} +function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = (...args) => { + let node = args[0]; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } +} +function isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); +} +function isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); +} +function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); +} +class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + } + getId(stylesheet) { + var _a; + return (_a = this.styleIDMap.get(stylesheet)) !== null && _a !== void 0 ? _a : -1; + } + has(stylesheet) { + return this.styleIDMap.has(stylesheet); + } + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; + } + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + getStyle(id) { + return this.idStyleMap.get(id) || null; + } + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; + } + generateId() { + return this.id++; + } +} +function getShadowHost(n) { + var _a, _b; + let shadowHost = null; + if (((_b = (_a = n.getRootNode) === null || _a === void 0 ? void 0 : _a.call(n)) === null || _b === void 0 ? void 0 : _b.nodeType) === Node.DOCUMENT_FRAGMENT_NODE && + n.getRootNode().host) + shadowHost = n.getRootNode().host; + return shadowHost; +} +function getRootShadowHost(n) { + let rootShadowHost = n; + let shadowHost; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; + return rootShadowHost; +} +function shadowHostInDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + const shadowHost = getRootShadowHost(n); + return doc.contains(shadowHost); +} +function inDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + return doc.contains(n) || shadowHostInDom(n); +} + +var EventType$1 = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; +})(EventType$1 || {}); +var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; +})(IncrementalSource$1 || {}); +var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; +})(MouseInteractions || {}); +var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; +})(PointerTypes || {}); +var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; +})(CanvasContext || {}); + +function isNodeInLinkedList(n) { + return '__ln' in n; +} +class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + this.tail = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + if (node.next === null) { + this.tail = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + else { + this.tail = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } +} +const moveKey = (id, parentId) => `${id}@${parentId}`; +class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addedIds = new Set(); + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + if (!n.parentNode || + !inDom(n) || + n.parentNode.tagName === 'TEXTAREA') { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + let tailNode = addList.tail; + while (tailNode) { + const _node = tailNode; + tailNode = tailNode.previous; + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (nextId === -1) + continue; + else if (parentId !== -1) { + node = _node; + break; + } + else { + const unhandledNode = _node.value; + if (unhandledNode.parentNode && + unhandledNode.parentNode.nodeType === + Node.DOCUMENT_FRAGMENT_NODE) { + const shadowHost = unhandledNode.parentNode + .host; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => { + const n = text.node; + if (n.parentNode && + n.parentNode.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(n.parentNode); + } + return { + id: this.mirror.getId(n), + value: text.value, + }; + }) + .filter((text) => !addedIds.has(text.id)) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => { + const { attributes } = attribute; + if (typeof attributes.style === 'string') { + const diffAsStr = JSON.stringify(attribute.styleDiff); + const unchangedAsStr = JSON.stringify(attribute._unchangedStyles); + if (diffAsStr.length < attributes.style.length) { + if ((diffAsStr + unchangedAsStr).split('var(').length === + attributes.style.split('var(').length) { + attributes.style = attribute.styleDiff; + } + } + } + return { + id: this.mirror.getId(attribute.node), + attributes: attributes, + }; + }) + .filter((attribute) => !addedIds.has(attribute.id)) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.genTextAreaValueMutation = (textarea) => { + let item = this.attributeMap.get(textarea); + if (!item) { + item = { + node: textarea, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(textarea, item); + } + item.attributes.value = Array.from(textarea.childNodes, (cn) => cn.textContent || '').join(''); + }; + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, false) && + value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, true) && value + ? this.maskTextFn + ? this.maskTextFn(value, closestElementOfNode(m.target)) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let attributeName = m.attributeName; + let value = m.target.getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ + element: target, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type, + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + value === m.oldValue) { + return; + } + let item = this.attributeMap.get(m.target); + if (target.tagName === 'IFRAME' && + attributeName === 'src' && + !this.keepIframeSrcFn(value)) { + if (!target.contentDocument) { + attributeName = 'rr_src'; + } + else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(m.target, item); + } + if (attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (!ignoreAttribute(target.tagName, attributeName)) { + item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value); + if (attributeName === 'style') { + if (!this.unattachedDoc) { + try { + this.unattachedDoc = + document.implementation.createHTMLDocument(); + } + catch (e) { + this.unattachedDoc = this.doc; + } + } + const old = this.unattachedDoc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + item.styleDiff[pname] = newValue; + } + else { + item.styleDiff[pname] = [newValue, newPriority]; + } + } + else { + item._unchangedStyles[pname] = [newValue, newPriority]; + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + item.styleDiff[pname] = false; + } + } + } + } + break; + } + case 'childList': { + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; + if (m.target.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(m.target); + return; + } + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (this.processedNodeManager.inOtherBuffer(n, this)) + return; + if (this.addedSet.has(n) || this.movedSet.has(n)) + return; + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); + } + } + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'dataURLOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'processedNodeManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } +} +function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); +} +function isParentRemoved(removes, n, mirror) { + if (removes.length === 0) + return false; + return _isParentRemoved(removes, n, mirror); +} +function _isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); +} +function isAncestorInSet(set, n) { + if (set.size === 0) + return false; + return _isAncestorInSet(set, n); +} +function _isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); +} + +let errorHandler; +function registerErrorHandler(handler) { + errorHandler = handler; +} +function unregisterErrorHandler() { + errorHandler = undefined; +} +const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { + try { + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return; + } + throw error; + } + }); + return rrwebWrapped; +}; + +const mutationBuffers = []; +function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { + } + return event && event.target; +} +function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer))); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; +} +function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { + }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle(callbackWrapper((source) => { + const totalOffset = Date.now() - timeBaseline; + mousemoveCb(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }), callbackThreshold); + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = legacy_isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = nowTimestamp(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: nowTimestamp() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource$1.Drag + : evt instanceof MouseEvent + ? IncrementalSource$1.MouseMove + : IncrementalSource$1.TouchMove); + }), threshold, { + trailing: false, + })); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { + }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + let currentPointerType = null; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, true)) { + return; + } + let pointerType = null; + let thisEventKey = eventKey; + if ('pointerType' in event) { + switch (event.pointerType) { + case 'mouse': + pointerType = PointerTypes.Mouse; + break; + case 'touch': + pointerType = PointerTypes.Touch; + break; + case 'pen': + pointerType = PointerTypes.Pen; + break; + } + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + thisEventKey = 'TouchStart'; + } + else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) { + thisEventKey = 'TouchEnd'; + } + } + else if (pointerType === PointerTypes.Pen) ; + } + else if (legacy_isTouchEvent(event)) { + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + if ((thisEventKey.startsWith('Touch') && + pointerType === PointerTypes.Touch) || + (thisEventKey.startsWith('Mouse') && + pointerType === PointerTypes.Mouse)) { + pointerType = null; + } + } + else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; + } + const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)(Object.assign({ type: MouseInteractions[thisEventKey], id, x: clientX, y: clientY }, (pointerType !== null && { pointerType }))); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + let eventName = toLowerCase(eventKey); + const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + return; + } + } + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }) { + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); + } + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }), sampling.scroll || 100)); + return on('scroll', updatePosition, doc); +} +function initViewportResizeObserver({ viewportResizeCb }, { win }) { + let lastH = -1; + let lastW = -1; + const updateDimension = callbackWrapper(throttle(callbackWrapper(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }), 200)); + return on('resize', updateDimension, win); +} +const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; +const lastInputValueMap = new WeakMap(); +function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + if (target.classList.contains(ignoreClass) || + (ignoreSelector && target.matches(ignoreSelector))) { + return; + } + let text = target.value; + let isChecked = false; + const type = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + else if (maskInputOptions[tagName.toLowerCase()] || + maskInputOptions[type]) { + text = maskInputValue({ + element: target, + maskInputOptions, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, userTriggeredOnInput + ? { text, isChecked, userTriggered } + : { text, isChecked }); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = el.value; + cbWithDedup(el, userTriggeredOnInput + ? { text, isChecked: !isChecked, userTriggered: false } + : { text, isChecked: !isChecked }); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + callbackWrapper(inputCb)(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const currentWindow = doc.defaultView; + if (!currentWindow) { + return () => { + handlers.forEach((h) => h()); + }; + } + const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [currentWindow.HTMLInputElement.prototype, 'value'], + [currentWindow.HTMLInputElement.prototype, 'checked'], + [currentWindow.HTMLSelectElement.prototype, 'value'], + [currentWindow.HTMLTextAreaElement.prototype, 'value'], + [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'], + [currentWindow.HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ + target: this, + isTrusted: false, + }); + }, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); +} +function getIdAndStyleId(sheet, mirror, styleMirror) { + let id, styleId; + if (!sheet) + return {}; + if (sheet.ownerNode) + id = mirror.getId(sheet.ownerNode); + else + styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; +} +function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { + }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + let replace; + if (win.CSSStyleSheet.prototype.replace) { + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = new Proxy(replace, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + let replaceSync; + if (win.CSSStyleSheet.prototype.replaceSync) { + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); +} +function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) { + var _a, _b, _c; + let hostId = null; + if (host.nodeName === '#document') + hostId = mirror.getId(host); + else + hostId = mirror.getId(host.host); + const patchTarget = host.nodeName === '#document' + ? (_a = host.defaultView) === null || _a === void 0 ? void 0 : _a.Document + : (_c = (_b = host.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.ShadowRoot; + const originalPropertyDescriptor = (patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype) + ? Object.getOwnPropertyDescriptor(patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype, 'adoptedStyleSheets') + : undefined; + if (hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor) + return () => { + }; + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get() { + var _a; + return (_a = originalPropertyDescriptor.get) === null || _a === void 0 ? void 0 : _a.call(this); + }, + set(sheets) { + var _a; + const result = (_a = originalPropertyDescriptor.set) === null || _a === void 0 ? void 0 : _a.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } + catch (e) { + } + } + return result; + }, + }); + return callbackWrapper(() => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get: originalPropertyDescriptor.get, + set: originalPropertyDescriptor.set, + }); + }); +} +function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property, value, priority] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return setProperty.apply(thisArg, [property, value, priority]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return removeProperty.apply(thisArg, [property]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); +} +function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, mirror, sampling, doc, }) { + const handler = callbackWrapper((type) => throttle(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const { currentTime, volume, muted, playbackRate, loop } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + playbackRate, + loop, + }); + }), sampling.media || 500)); + const handlers = [ + on('play', handler(0), doc), + on('pause', handler(1), doc), + on('seeked', handler(2), doc), + on('volumechange', handler(3), doc), + on('ratechange', handler(4), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { + }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(callbackWrapper(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }), 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initSelectionObserver(param) { + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; + let collapsed = true; + const updateSelection = callbackWrapper(() => { + const selection = doc.getSelection(); + if (!selection || (collapsed && (selection === null || selection === void 0 ? void 0 : selection.isCollapsed))) + return; + collapsed = selection.isCollapsed || false; + const ranges = []; + const count = selection.rangeCount || 0; + for (let i = 0; i < count; i++) { + const range = selection.getRangeAt(i); + const { startContainer, startOffset, endContainer, endOffset } = range; + const blocked = isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); + if (blocked) + continue; + ranges.push({ + start: mirror.getId(startContainer), + startOffset, + end: mirror.getId(endContainer), + endOffset, + }); + } + selectionCb({ ranges }); + }); + updateSelection(); + return on('selectionchange', updateSelection); +} +function initCustomElementObserver({ doc, customElementCb, }) { + const win = doc.defaultView; + if (!win || !win.customElements) + return () => { }; + const restoreHandler = patch(win.customElements, 'define', function (original) { + return function (name, constructor, options) { + try { + customElementCb({ + define: { + name, + }, + }); + } + catch (e) { + console.warn(`Custom element callback failed for ${name}`); + } + return original.apply(this, [name, constructor, options]); + }; + }); + return restoreHandler; +} +function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, selectionCb, customElementCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; + o.selectionCb = (...p) => { + if (hooks.selection) { + hooks.selection(...p); + } + selectionCb(...p); + }; + o.customElementCb = (...c) => { + if (hooks.customElement) { + hooks.customElement(...c); + } + customElementCb(...c); + }; +} +function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { + }; + } + mergeHooks(o, hooks); + let mutationObserver; + if (o.recordDOM) { + mutationObserver = initMutationObserver(o, o.doc); + } + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o, { + win: currentWindow, + }); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + let styleSheetObserver = () => { }; + let adoptedStyleSheetObserver = () => { }; + let styleDeclarationObserver = () => { }; + let fontObserver = () => { }; + if (o.recordDOM) { + styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); + styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + if (o.collectFonts) { + fontObserver = initFontObserver(o); + } + } + const selectionObserver = initSelectionObserver(o); + const customElementObserver = initCustomElementObserver(o); + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver === null || mutationObserver === void 0 ? void 0 : mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + adoptedStyleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + selectionObserver(); + customElementObserver(); + pluginHandlers.forEach((h) => h()); + }); +} +function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; +} +function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); +} + +class CrossOriginIframeMirror { + constructor(generateIdFn) { + this.generateIdFn = generateIdFn; + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + } + getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) { + const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe); + let id = idToRemoteIdMap.get(remoteId); + if (!id) { + id = this.generateIdFn(); + idToRemoteIdMap.set(remoteId, id); + remoteIdToIdMap.set(id, remoteId); + } + return id; + } + getIds(iframe, remoteId) { + const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + } + getRemoteId(iframe, id, map) { + const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe); + if (typeof id !== 'number') + return id; + const remoteId = remoteIdToIdMap.get(id); + if (!remoteId) + return -1; + return remoteId; + } + getRemoteIds(iframe, ids) { + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + } + reset(iframe) { + if (!iframe) { + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + return; + } + this.iframeIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToIdMap.delete(iframe); + } + getIdToRemoteIdMap(iframe) { + let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); + if (!idToRemoteIdMap) { + idToRemoteIdMap = new Map(); + this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + } + return idToRemoteIdMap; + } + getRemoteIdToIdMap(iframe) { + let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); + if (!remoteIdToIdMap) { + remoteIdToIdMap = new Map(); + this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + } + return remoteIdToIdMap; + } +} + +class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.crossOriginIframeMap = new WeakMap(); + this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + this.crossOriginIframeRootIdMap = new WeakMap(); + this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; + this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)); + this.mirror = options.mirror; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + if (iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0) + this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument)); + } + handleMessage(message) { + const crossOriginMessageEvent = message; + if (crossOriginMessageEvent.data.type !== 'rrweb' || + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin) + return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) + return; + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) + return; + const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event); + if (transformedEvent) + this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout); + } + transformCrossOriginEvent(iframeEl, e) { + var _a; + switch (e.type) { + case EventType$1.FullSnapshot: { + this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); + this.replaceIdOnNode(e.data.node, iframeEl); + const rootId = e.data.node.id; + this.crossOriginIframeRootIdMap.set(iframeEl, rootId); + this.patchRootIdOnNode(e.data.node, rootId); + return { + timestamp: e.timestamp, + type: EventType$1.IncrementalSnapshot, + data: { + source: IncrementalSource$1.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + case EventType$1.Meta: + case EventType$1.Load: + case EventType$1.DomContentLoaded: { + return false; + } + case EventType$1.Plugin: { + return e; + } + case EventType$1.Custom: { + this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); + return e; + } + case EventType$1.IncrementalSnapshot: { + switch (e.data.source) { + case IncrementalSource$1.Mutation: { + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, [ + 'parentId', + 'nextId', + 'previousId', + ]); + this.replaceIdOnNode(n.node, iframeEl); + const rootId = this.crossOriginIframeRootIdMap.get(iframeEl); + rootId && this.patchRootIdOnNode(n.node, rootId); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.Drag: + case IncrementalSource$1.TouchMove: + case IncrementalSource$1.MouseMove: { + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.ViewportResize: { + return false; + } + case IncrementalSource$1.MediaInteraction: + case IncrementalSource$1.MouseInteraction: + case IncrementalSource$1.Scroll: + case IncrementalSource$1.CanvasMutation: + case IncrementalSource$1.Input: { + this.replaceIds(e.data, iframeEl, ['id']); + return e; + } + case IncrementalSource$1.StyleSheetRule: + case IncrementalSource$1.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleId']); + return e; + } + case IncrementalSource$1.Font: { + return e; + } + case IncrementalSource$1.Selection: { + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); + return e; + } + case IncrementalSource$1.AdoptedStyleSheet: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); + return e; + } + } + } + } + return false; + } + replace(iframeMirror, obj, iframeEl, keys) { + for (const key of keys) { + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') + continue; + if (Array.isArray(obj[key])) { + obj[key] = iframeMirror.getIds(iframeEl, obj[key]); + } + else { + obj[key] = iframeMirror.getId(iframeEl, obj[key]); + } + } + return obj; + } + replaceIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); + } + replaceStyleIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); + } + replaceIdOnNode(node, iframeEl) { + this.replaceIds(node, iframeEl, ['id', 'rootId']); + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } + patchRootIdOnNode(node, rootId) { + if (node.type !== NodeType.Document && !node.rootId) + node.rootId = rootId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.patchRootIdOnNode(child, rootId); + }); + } + } +} + +class ShadowDomManager { + constructor(options) { + this.shadowDoms = new WeakSet(); + this.restoreHandlers = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + this.init(); + } + init() { + this.reset(); + this.patchAttachShadow(Element, document); + } + addShadowRoot(shadowRoot, doc) { + if (!isNativeShadowDom(shadowRoot)) + return; + if (this.shadowDoms.has(shadowRoot)) + return; + this.shadowDoms.add(shadowRoot); + const observer = initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + this.restoreHandlers.push(() => observer.disconnect()); + this.restoreHandlers.push(initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror }))); + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0) + this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host)); + this.restoreHandlers.push(initAdoptedStyleSheetObserver({ + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, shadowRoot)); + }, 0); + } + observeAttachShadow(iframeElement) { + if (!iframeElement.contentWindow || !iframeElement.contentDocument) + return; + this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument); + } + patchAttachShadow(element, doc) { + const manager = this; + this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) { + return function (option) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot && inDom(this)) + manager.addShadowRoot(this.shadowRoot, doc); + return shadowRoot; + }; + })); + } + reset() { + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } + catch (e) { + } + }); + this.restoreHandlers = []; + this.shadowDoms = new WeakSet(); + } +} + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +} + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +/* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ +var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +// Use a lookup table to find the index. +var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); +for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; +} +var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; +}; + +const canvasVarMap = new Map(); +function variableListFor(ctx, ctor) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); +} +const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; +}; +function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + const src = value.toDataURL(); + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; +} +const serializeArgs = (args, win, ctx) => { + return args.map((arg) => serializeArg(arg, win, ctx)); +}; +const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); +}; + +function initCanvas2DMutationObserver(cb, win, blockClass, blockSelector) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { + setTimeout(() => { + const recordArgs = serializeArgs(args, win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function getNormalizedContextName(contextType) { + return contextType === 'experimental-webgl' ? 'webgl' : contextType; +} +function initCanvasContextObserver(win, blockClass, blockSelector, setPreserveDrawingBufferToTrue) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, true)) { + const ctxName = getNormalizedContextName(contextType); + if (!('__context' in this)) + this.__context = ctxName; + if (setPreserveDrawingBufferToTrue && + ['webgl', 'webgl2'].includes(ctxName)) { + if (args[0] && typeof args[0] === 'object') { + const contextAttributes = args[0]; + if (!contextAttributes.preserveDrawingBuffer) { + contextAttributes.preserveDrawingBuffer = true; + } + } + else { + args.splice(0, 1, { + preserveDrawingBuffer: true, + }); + } + } + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function patchGLPrototype(prototype, type, cb, blockClass, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + if ([ + 'isContextLost', + 'canvas', + 'drawingBufferWidth', + 'drawingBufferHeight', + ].includes(prop)) { + continue; + } + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, this); + if ('tagName' in this.canvas && + !isBlocked(this.canvas, blockClass, blockSelector, true)) { + const recordArgs = serializeArgs(args, win, this); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; +} +function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function funcToSource(fn, sourcemapArg) { + var sourcemap = sourcemapArg === undefined ? null : sourcemapArg; + var source = fn.toString(); + var lines = source.split('\n'); + lines.pop(); + lines.shift(); + var blankPrefixLength = lines[0].search(/\S/); + var regex = /(['"])__worker_loader_strict__(['"])/g; + for (var i = 0, n = lines.length; i < n; ++i) { + lines[i] = lines[i].substring(blankPrefixLength).replace(regex, '$1use strict$2') + '\n'; + } + if (sourcemap) { + lines.push('\/\/# sourceMappingURL=' + sourcemap + '\n'); + } + return lines; +} + +function createURL(fn, sourcemapArg) { + var lines = funcToSource(fn, sourcemapArg); + var blob = new Blob(lines, { type: 'application/javascript' }); + return URL.createObjectURL(blob); +} + +function createInlineWorkerFactory(fn, sourcemapArg) { + var url; + return function WorkerFactory(options) { + url = url || createURL(fn, sourcemapArg); + return new Worker(url, options); + }; +} + +var WorkerFactory = createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () { +(function () { + '__worker_loader_strict__'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const lastBlobMap = new Map(); + const transparentBlobMap = new Map(); + function getTransparentBlobFor(width, height, dataURLOptions) { + return __awaiter(this, void 0, void 0, function* () { + const id = `${width}-${height}`; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) + return transparentBlobMap.get(id); + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + transparentBlobMap.set(id, base64); + return base64; + } + else { + return ''; + } + }); + } + const worker = self; + worker.onmessage = function (e) { + return __awaiter(this, void 0, void 0, function* () { + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor(width, height, dataURLOptions); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const type = blob.type; + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + if (!lastBlobMap.has(id) && (yield transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + if (lastBlobMap.get(id) === base64) + return worker.postMessage({ id }); + worker.postMessage({ + id, + type, + base64, + width, + height, + }); + lastBlobMap.set(id, base64); + } + else { + return worker.postMessage({ id: e.data.id }); + } + }); + }; + +})(); +}, null); + +class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = (target, mutation) => { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas, dataURLOptions, } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass, blockSelector); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { + dataURLOptions, + }); + } + initCanvasFPSObserver(fps, win, blockClass, blockSelector, options) { + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, true); + const snapshotInProgressMap = new Map(); + const worker = new WorkerFactory(); + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + if (!('base64' in e.data)) + return; + const { base64, type, width, height } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', + args: [0, 0, width, height], + }, + { + property: 'drawImage', + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + }, + 0, + 0, + ], + }, + ], + }); + }; + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId; + const getCanvas = () => { + const matchedCanvas = []; + win.document.querySelectorAll('canvas').forEach((canvas) => { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { + matchedCanvas.push(canvas); + } + }); + return matchedCanvas; + }; + const takeCanvasSnapshots = (timestamp) => { + if (lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + getCanvas() + .forEach((canvas) => __awaiter(this, void 0, void 0, function* () { + var _a; + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) + return; + if (canvas.width === 0 || canvas.height === 0) + return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes(canvas.__context)) { + const context = canvas.getContext(canvas.__context); + if (((_a = context === null || context === void 0 ? void 0 : context.getContextAttributes()) === null || _a === void 0 ? void 0 : _a.preserveDrawingBuffer) === false) { + context.clear(context.COLOR_BUFFER_BIT); + } + } + const bitmap = yield createImageBitmap(canvas); + worker.postMessage({ + id, + bitmap, + width: canvas.width, + height: canvas.height, + dataURLOptions: options.dataURLOptions, + }, [bitmap]); + })); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + rafId = requestAnimationFrame(takeCanvasSnapshots); + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + initCanvasMutationObserver(win, blockClass, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, false); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } +} + +class StylesheetManager { + constructor(options) { + this.trackedLinkElements = new WeakSet(); + this.styleMirror = new StyleSheetMirror(); + this.mutationCb = options.mutationCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; + } + attachLinkElement(linkEl, childSn) { + if ('_cssText' in childSn.attributes) + this.mutationCb({ + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: childSn.id, + attributes: childSn + .attributes, + }, + ], + }); + this.trackLinkElement(linkEl); + } + trackLinkElement(linkEl) { + if (this.trackedLinkElements.has(linkEl)) + return; + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + adoptStyleSheets(sheets, hostId) { + if (sheets.length === 0) + return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [], + }; + const styles = []; + for (const sheet of sheets) { + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + styles.push({ + styleId, + rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ + rule: stringifyRule(r), + index, + })), + }); + } + else + styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); + } + if (styles.length > 0) + adoptedStyleSheetData.styles = styles; + this.adoptedStyleSheetCb(adoptedStyleSheetData); + } + reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + trackStylesheetInLinkElement(linkEl) { + } +} + +class ProcessedNodeManager { + constructor() { + this.nodeMap = new WeakMap(); + this.loop = true; + this.periodicallyClear(); + } + periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + if (this.loop) + this.periodicallyClear(); + }); + } + inOtherBuffer(node, thisBuffer) { + const buffers = this.nodeMap.get(node); + return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + } + add(node, buffer) { + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + } + clear() { + this.nodeMap = new WeakMap(); + } + destroy() { + this.loop = false; + } +} + +function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: nowTimestamp() }); +} +let wrappedEmit; +let takeFullSnapshot; +let canvasManager; +let recording = false; +const mirror = createMirror(); +function record(options = {}) { + const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordDOM = true, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded' + ? options.recordAfter + : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; + registerErrorHandler(errorHandler); + const inEmittingFrame = recordCrossOriginIframes + ? window.parent === window + : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + if (window.parent.document) { + passEmitsToParent = false; + } + } + catch (e) { + passEmitsToParent = true; + } + } + if (inEmittingFrame && !emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + mirror.reset(); + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn && + !passEmitsToParent) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType$1.FullSnapshot && + !(e.type === EventType$1.IncrementalSnapshot && + e.data.source === IncrementalSource$1.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + if (inEmittingFrame) { + emit === null || emit === void 0 ? void 0 : emit(eventProcessor(e), isCheckout); + } + else if (passEmitsToParent) { + const message = { + type: 'rrweb', + event: eventProcessor(e), + origin: window.location.origin, + isCheckout, + }; + window.parent.postMessage(message, '*'); + } + if (e.type === EventType$1.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType$1.IncrementalSnapshot) { + if (e.data.source === IncrementalSource$1.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p), + })); + const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a), + })); + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, + }); + const iframeManager = new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + }); + } + const processedNodeManager = new ProcessedNodeManager(); + canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + if (!recordDOM) { + return; + } + wrappedEmit(wrapEvent({ + type: EventType$1.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + stylesheetManager.reset(); + shadowDomManager.init(); + mutationBuffers.forEach((buf) => buf.lock()); + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + dataURLOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.trackLinkElement(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachLinkElement(linkEl, childSn); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.FullSnapshot, + data: { + node, + initialOffset: getWindowScroll(window), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.unlock()); + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document)); + }; + try { + const handlers = []; + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Font }, p), + })), + selectionCb: (p) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Selection }, p), + })); + }, + customElementCb: (c) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CustomElement }, c), + })); + }, + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordDOM, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + dataURLOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + processedNodeManager, + canvasManager, + ignoreCSSAttributes, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType$1.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + recording = true; + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.DomContentLoaded, + data: {}, + })); + if (recordAfter === 'DOMContentLoaded') + init(); + })); + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.Load, + data: {}, + })); + if (recordAfter === 'load') + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + processedNodeManager.destroy(); + recording = false; + unregisterErrorHandler(); + }; + } + catch (error) { + console.warn(error); + } +} +record.addCustomEvent = (tag, payload) => { + if (!recording) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.Custom, + data: { + tag, + payload, + }, + })); +}; +record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); +}; +record.takeFullSnapshot = (isCheckout) => { + if (!recording) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); +}; +record.mirror = mirror; + +var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; +})(EventType || {}); +var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; +})(IncrementalSource || {}); + var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -62,7 +4569,7 @@ var _ = { }; // Console override -var console = { +var console$1 = { /** @type {function(...*)} */ log: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { @@ -119,14 +4626,14 @@ var console = { var log_func_with_prefix = function(func, prefix) { return function() { arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); + return func.apply(console$1, arguments); }; }; var console_with_prefix = function(prefix) { return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) + log: log_func_with_prefix(console$1.log, prefix), + error: log_func_with_prefix(console$1.error, prefix), + critical: log_func_with_prefix(console$1.critical, prefix) }; }; @@ -979,7 +5486,7 @@ _.getQueryParam = function(url, param) { try { result = decodeURIComponent(result); } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); + console$1.error('Skipping decoding for malformed query param: ' + result); } return result.replace(/\+/g, ' '); } @@ -1106,13 +5613,13 @@ _.localStorage = { is_supported: function(force_check) { var supported = localStorageSupported(null, force_check); if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); + console$1.error('localStorage unsupported; falling back to cookie store'); } return supported; }, error: function(msg) { - console.error('localStorage error: ' + msg); + console$1.error('localStorage error: ' + msg); }, get: function(name) { @@ -1167,7 +5674,7 @@ _.register_event = (function() { */ var register_event = function(element, type, handler, oldSchool, useCapture) { if (!element) { - console.error('No valid element provided to register_event'); + console$1.error('No valid element provided to register_event'); return; } @@ -1729,158 +6236,306 @@ _['info']['browser'] = _.info.browser; _['info']['browserVersion'] = _.info.browserVersion; _['info']['properties'] = _.info.properties; -/* eslint camelcase: "off" */ - /** - * DomTracker Object - * @constructor + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. */ -var DomTracker = function() {}; - - -// interface -DomTracker.prototype.create_properties = function() {}; -DomTracker.prototype.event_handler = function() {}; -DomTracker.prototype.after_track_handler = function() {}; - -DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; -}; /** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ -DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - that.event_handler(e, this, options); +/** Public **/ - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} - return true; -}; +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} /** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type */ -DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console$1.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console$1.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} - that.after_track_handler(props, options, timeout_occured); - }; -}; +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} -DomTracker.prototype.create_properties = function(properties, element) { - var props; +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} - return props; -}; +/** Private **/ /** - * LinkTracker Object - * @constructor - * @extends DomTracker + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage */ -var LinkTracker = function() { - this.override_event = 'click'; -}; -_.inherit(LinkTracker, DomTracker); +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} -LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} - if (element.href) { props['url'] = element.href; } +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} - return props; -}; +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; -LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); - if (!options.new_tab) { - evt.preventDefault(); + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console$1.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; } -}; -LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } + options = options || {}; - setTimeout(function() { - window.location = options.href; - }, 0); -}; + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} /** - * FormTracker Object - * @constructor - * @extends DomTracker + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out */ -var FormTracker = function() { - this.override_event = 'submit'; -}; -_.inherit(FormTracker, DomTracker); +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; -FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); -}; + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests -FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); -}; + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console$1.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } -var logger$2 = console_with_prefix('lock'); + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +var logger$3 = console_with_prefix('lock'); /** * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser @@ -1937,7 +6592,7 @@ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { var delay = function(cb) { if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); storage.removeItem(keyZ); storage.removeItem(keyY); loop(); @@ -2026,7 +6681,7 @@ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { } }; -var logger$1 = console_with_prefix('batch'); +var logger$2 = console_with_prefix('batch'); /** * RequestQueue: queue for batching API requests with localStorage backup for retries. @@ -2048,9 +6703,10 @@ var RequestQueue = function(storageKey, options) { options = options || {}; this.storageKey = storageKey; this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2075,29 +6731,36 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2108,7 +6771,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2157,61 +6820,67 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2239,25 +6908,32 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } + if (!this.usePersistence) { if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2300,13 +6976,16 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes -var logger = console_with_prefix('batch'); +var logger$1 = console_with_prefix('batch'); /** * RequestBatcher: manages the queueing, flushing, retry etc of requests of one @@ -2318,7 +6997,8 @@ var RequestBatcher = function(storageKey, options) { this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2335,6 +7015,11 @@ var RequestBatcher = function(storageKey, options) { // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2410,7 +7095,7 @@ RequestBatcher.prototype.flush = function(options) { try { if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); + logger$1.log('Flush: Request already in progress'); return; } @@ -2419,6 +7104,9 @@ RequestBatcher.prototype.flush = function(options) { var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2486,22 +7174,17 @@ RequestBatcher.prototype.flush = function(options) { this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2525,7 +7208,11 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2571,9 +7258,8 @@ RequestBatcher.prototype.flush = function(options) { if (options.unloading) { requestOptions.transport = 'sendBeacon'; } - logger.log('MIXPANEL REQUEST:', dataForRequest); + logger$1.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2584,7 +7270,7 @@ RequestBatcher.prototype.flush = function(options) { * Log error to global logger and optional user-defined logger. */ RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); + logger$1.error.apply(logger$1.error, arguments); if (this.errorReporter) { try { if (!(err instanceof Error)) { @@ -2592,309 +7278,402 @@ RequestBatcher.prototype.reportError = function(msg, err) { } this.errorReporter(msg, err); } catch(err) { - logger.error(err); + logger$1.error(err); } } }; -/** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - -/** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ +var logger = console_with_prefix('recorder'); +var CompressionStream = win['CompressionStream']; -/** Public **/ +var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true +}; -var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; +var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, +]); -/** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function optIn(token, options) { - _optInOut(true, token, options); +function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); } -/** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ -function optOut(token, options) { - _optInOut(false, token, options); -} +var MixpanelRecorder = function(mixpanelInstance) { + this._mixpanel = mixpanelInstance; -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ -function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; -} + // internal rrweb stopRecording function + this._stopRecording = null; -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ -function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; + this.recEvents = []; + this.seqNo = 0; + this.replayId = null; + this.replayStartTime = null; + this.sendBatchId = null; + + this.idleTimeoutId = null; + this.maxTimeoutId = null; + + this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); +}; + + +MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); +}; + +// eslint-disable-next-line camelcase +MixpanelRecorder.prototype.get_config = function(configVar) { + return this._mixpanel.get_config(configVar); +}; + +MixpanelRecorder.prototype.startRecording = function () { + if (this._stopRecording !== null) { + logger.log('Recording already in progress, skipping startRecording.'); + return; } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + + this.recordMaxMs = this.get_config('record_max_ms'); + if (this.recordMaxMs > MAX_RECORDING_MS) { + this.recordMaxMs = MAX_RECORDING_MS; + logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.'); } - return optedOut; -} -/** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); -} + this.recEvents = []; + this.seqNo = 0; + this.replayStartTime = null; -/** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + this.replayId = _.UUID(); + + this.batcher.start(); + + var resetIdleTimeout = _.bind(function () { + clearTimeout(this.idleTimeoutId); + this.idleTimeoutId = setTimeout(_.bind(function () { + logger.log('Idle timeout reached, restarting recording.'); + this.resetRecording(); + }, this), this.get_config('record_idle_timeout_ms')); + }, this); + + this._stopRecording = record({ + 'emit': _.bind(function (ev) { + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } + }, this), + 'blockClass': this.get_config('record_block_class'), + 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, + 'maskTextClass': this.get_config('record_mask_text_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); -} + + resetIdleTimeout(); + + this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); +}; + +MixpanelRecorder.prototype.resetRecording = function () { + this.stopRecording(); + this.startRecording(); +}; + +MixpanelRecorder.prototype.stopRecording = function () { + if (this._stopRecording !== null) { + this._stopRecording(); + this._stopRecording = null; + } + + this.batcher.flush(); // flush any remaining events + this.replayId = null; + + clearTimeout(this.idleTimeoutId); + clearTimeout(this.maxTimeoutId); +}; /** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out + * Flushes the current batch of events to the server, but passes an opt-out callback to make sure + * we stop recording and dump any queued events if the user has opted out. */ -function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); +MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); +}; + +MixpanelRecorder.prototype._onOptOut = function (code) { + // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out + if (code === 0) { + this.recEvents = []; + this.stopRecording(); + } +}; + +MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/octet-stream' + }, + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); -} +}; -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); -} +MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + + var reqParams = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + var eventsJson = _.JSONEncode(data); + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqParams['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqParams['$user_id'] = userId; + } + + if (CompressionStream) { + var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); + var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); + new Response(gzipStream) + .blob() + .then(_.bind(function(compressedBlob) { + reqParams['format'] = 'gzip'; + this._sendRequest(reqParams, compressedBlob, callback); + }, this)); + } else { + reqParams['format'] = 'body'; + this._sendRequest(reqParams, eventsJson, callback); + } + } +}); + + +MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } +}; -/** Private **/ -/** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ -function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; -} +win['__mp_recorder'] = MixpanelRecorder; -/** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ -function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; -} +/* eslint camelcase: "off" */ /** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type + * DomTracker Object + * @constructor */ -function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); -} +var DomTracker = function() {}; -/** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ -function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; - return hasDntOn; -} +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; /** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback */ -function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); return; } - options = options || {}; + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); + that.event_handler(e, this, options); - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); }); - } -} + }, this); + + return true; +}; /** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured */ -function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; } - if (!optedOut) { - return method.apply(this, arguments); - } + that.after_track_handler(props, options, timeout_occured); + }; +}; - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } +DomTracker.prototype.create_properties = function(properties, element) { + var props; - return; - }; -} + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; /* eslint camelcase: "off" */ @@ -3316,7 +8095,7 @@ MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); return; } else { $add[k] = v; @@ -3441,7 +8220,7 @@ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(am if (!_.isNumber(amount)) { amount = parseFloat(amount); if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } @@ -3478,7 +8257,7 @@ MixpanelPeople.prototype.clear_charges = function(callback) { */ MixpanelPeople.prototype.delete_user = function() { if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); return; } var data = {'$delete': this._mixpanel.get_distinct_id()}; @@ -3552,7 +8331,7 @@ MixpanelPeople.prototype._enqueue = function(data) { } else if (UNION_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); } else { - console.error('Invalid call to _enqueue():', data); + console$1.error('Invalid call to _enqueue():', data); } }; @@ -3700,7 +8479,7 @@ var MixpanelPersistence = function(config) { var storage_type = config['persistence']; if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); storage_type = config['persistence'] = 'cookie'; } @@ -4004,8 +8783,8 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { this._pop_from_people_queue(UNSET_ACTION, q_data); } - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); this.save(); }; @@ -4050,7 +8829,7 @@ MixpanelPersistence.prototype._get_queue_key = function(queue) { } else if (queue === UNION_ACTION) { return UNION_QUEUE_KEY; } else { - console.error('Invalid queue:', queue); + console$1.error('Invalid queue:', queue); } }; @@ -4107,6 +8886,12 @@ Globals should be all caps */ var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -4200,7 +8985,9 @@ var DEFAULT_CONFIG = { 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4233,7 +9020,7 @@ var create_mplib = function(token, config, name) { instance = target; } else { if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); + console$1.error('You have already initialized ' + name); return; } instance = new MixpanelLib(); @@ -4361,9 +9148,9 @@ MixpanelLib.prototype._init = function(token, config, name) { if (this._batch_requests) { if (!_.localStorage.is_supported(true) || !USE_XHR) { this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); + console$1.log('Clearing batch queue ' + batcher_config.queue_key); _.localStorage.remove(batcher_config.queue_key); }); } else { @@ -4426,7 +9213,7 @@ MixpanelLib.prototype._init = function(token, config, name) { MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); + console$1.critical('Browser does not support MutationObserver; skipping session recording'); return; } @@ -4436,12 +9223,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(functi }, this); if (_.isUndefined(win['__mp_recorder'])) { - var scriptEl = document$1.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document$1.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -4451,7 +9233,7 @@ MixpanelLib.prototype.stop_session_recording = function () { if (this._recorder) { this._recorder['stopRecording'](); } else { - console.critical('Session recorder module not loaded'); + console$1.critical('Session recorder module not loaded'); } }; @@ -4740,7 +9522,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4840,6 +9623,7 @@ MixpanelLib.prototype.init_batchers = function() { attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4851,8 +9635,8 @@ MixpanelLib.prototype.init_batchers = function() { beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); @@ -4944,8 +9728,8 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) { truncated_data = this._run_hook('before_send_' + options.type, truncated_data); } if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); return this._send_request( endpoint, this._encode_data_for_request(truncated_data), @@ -6142,14 +10926,14 @@ MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { }; MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); + console$1.error.apply(console$1.error, arguments); try { if (!err && !(msg instanceof Error)) { msg = new Error(msg); } this.get_config('error_reporter')(msg, err); } catch(err) { - console.error(err); + console$1.error(err); } }; @@ -6301,7 +11085,8 @@ var add_dom_loaded_handler = function() { _.register_event(win, 'load', dom_loaded_handler, true); }; -function init_as_module() { +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); @@ -6312,8 +11097,15 @@ function init_as_module() { return mixpanel_master; } +// For loading separate bundles asynchronously via script tag + +// For builds that have everything in one bundle, no extra work. +function loadNoop (_src, onload) { + onload(); +} + /* eslint camelcase: "off" */ -var mixpanel = init_as_module(); +var mixpanel = init_as_module(loadNoop); module.exports = mixpanel; diff --git a/dist/mixpanel.globals.js b/dist/mixpanel.globals.js index 979211bf..241a6f11 100644 --- a/dist/mixpanel.globals.js +++ b/dist/mixpanel.globals.js @@ -3,7 +3,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -2052,6 +2052,7 @@ this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2076,29 +2077,36 @@ 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2109,7 +2117,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2158,61 +2166,67 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2240,25 +2254,32 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2301,7 +2322,10 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff @@ -2319,7 +2343,8 @@ this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2336,6 +2361,11 @@ // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2420,6 +2450,9 @@ var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2487,22 +2520,17 @@ this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2526,7 +2554,11 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2574,7 +2606,6 @@ } logger.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -4108,6 +4139,12 @@ */ var init_type; // MODULE or SNIPPET loader + // allow bundlers to specify how extra code (recorder bundle) should be loaded + // eslint-disable-next-line no-unused-vars + var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); + }; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -4201,7 +4238,9 @@ 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4437,12 +4476,7 @@ }, this); if (_.isUndefined(win['__mp_recorder'])) { - var scriptEl = document$1.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document$1.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -4741,7 +4775,8 @@ lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4841,6 +4876,7 @@ attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4852,8 +4888,8 @@ beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); @@ -6302,7 +6338,8 @@ _.register_event(win, 'load', dom_loaded_handler, true); }; - function init_from_snippet() { + function init_from_snippet(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_SNIPPET; mixpanel_master = win[PRIMARY_INSTANCE_NAME]; @@ -6342,8 +6379,19 @@ add_dom_loaded_handler(); } + // For loading separate bundles asynchronously via script tag + // so that we don't load them until they are needed at runtime. + function loadAsync (src, onload) { + var scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + scriptEl.async = true; + scriptEl.onload = onload; + scriptEl.src = src; + document.head.appendChild(scriptEl); + } + /* eslint camelcase: "off" */ - init_from_snippet(); + init_from_snippet(loadAsync); })(); diff --git a/dist/mixpanel.min.js b/dist/mixpanel.min.js index bf19d8f6..1570986e 100644 --- a/dist/mixpanel.min.js +++ b/dist/mixpanel.min.js @@ -1,112 +1,112 @@ (function() { var l=void 0,m=!0,r=null,D=!1; -(function(){function Aa(){function a(){if(!a.Dc)la=a.Dc=m,ma=D,c.a(F,function(a){a.qc()})}function b(){try{t.documentElement.doScroll("left")}catch(d){setTimeout(b,1);return}a()}if(t.addEventListener)"complete"===t.readyState?a():t.addEventListener("DOMContentLoaded",a,D);else if(t.attachEvent){t.attachEvent("onreadystatechange",a);var d=D;try{d=n.frameElement===r}catch(f){}t.documentElement.doScroll&&d&&b()}c.Tb(n,"load",a,m)}function Ba(){x.init=function(a,b,d){if(d)return x[d]||(x[d]=F[d]=S(a, -b,d),x[d].ka()),x[d];d=x;if(F.mixpanel)d=F.mixpanel;else if(a)d=S(a,b,"mixpanel"),d.ka(),F.mixpanel=d;x=d;1===ca&&(n.mixpanel=x);Ca()}}function Ca(){c.a(F,function(a,b){"mixpanel"!==b&&(x[b]=a)});x._=c}function da(a){a=c.e(a)?a:c.g(a)?{}:{days:a};return c.extend({},Da,a)}function S(a,b,d){var f,h="mixpanel"===d?x:x[d];if(h&&0===ca)f=h;else{if(h&&!c.isArray(h)){o.error("You have already initialized "+d);return}f=new e}f.kb={};f.X(a,b,d);f.people=new j;f.people.X(f);if(!f.c("skip_first_touch_marketing")){var a= -c.info.Y(r),g={},v=D;c.a(a,function(a,b){(g["initial_"+b]=a)&&(v=m)});v&&f.people.M(g)}J=J||f.c("debug");!c.g(h)&&c.isArray(h)&&(f.Aa.call(f.people,h.people),f.Aa(h));return f}function e(){}function P(){}function Ea(a){return a}function q(a){this.props={};this.Ad=D;this.name=a.persistence_name?"mp_"+a.persistence_name:"mp_"+a.token+"_mixpanel";var b=a.persistence;if("cookie"!==b&&"localStorage"!==b)o.A("Unknown persistence type "+b+"; falling back to cookie"),b=a.persistence="cookie";this.j="localStorage"=== -b&&c.localStorage.sa()?c.localStorage:c.cookie;this.load();this.kc(a);this.wd();this.save()}function j(){}function u(){}function C(a,b){this.J=b.J;this.ba=new G(a,{J:c.bind(this.h,this),j:b.j});this.B=b.B;this.Zc=b.$c;this.la=b.la;this.jd=b.kd;this.D=this.B.batch_size;this.pa=this.B.batch_flush_interval_ms;this.va=!this.B.batch_autostart;this.Ja=0;this.G={}}function na(a,b){var d=[];c.a(a,function(a){var c=a.id;if(c in b){if(c=b[c],c!==r)a.payload=c,d.push(a)}else d.push(a)});return d}function oa(a, -b){var d=[];c.a(a,function(a){a.id&&!b[a.id]&&d.push(a)});return d}function G(a,b){b=b||{};this.N=a;this.j=b.j||window.localStorage;this.h=b.J||c.bind(pa.error,pa);this.Xa=new qa(a,{j:this.j});this.ua=b.ua||r;this.H=[]}function qa(a,b){b=b||{};this.N=a;this.j=b.j||window.localStorage;this.Rb=b.Rb||100;this.ec=b.ec||2E3}function T(){this.Ob="submit"}function M(){this.Ob="click"}function E(){}function ra(a){var b=Fa,d=a.split("."),d=d[d.length-1];if(4a?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())};c.fa=function(a){var b={};c.a(a,function(a,f){c.Wa(a)&&0=i;)h()}function c(){var a,b,d="",f;if('"'=== -i)for(;h();){if('"'===i)return h(),d;if("\\"===i)if(h(),"u"===i){for(b=f=0;4>b;b+=1){a=parseInt(h(),16);if(!isFinite(a))break;f=16*f+a}d+=String.fromCharCode(f)}else if("string"===typeof k[i])d+=k[i];else break;else d+=i}g("Bad string")}function f(){var a;a="";"-"===i&&(a="-",h("-"));for(;"0"<=i&&"9">=i;)a+=i,h();if("."===i)for(a+=".";h()&&"0"<=i&&"9">=i;)a+=i;if("e"===i||"E"===i){a+=i;h();if("-"===i||"+"===i)a+=i,h();for(;"0"<=i&&"9">=i;)a+=i,h()}a=+a;if(isFinite(a))return a;g("Bad number")}function h(a){a&& -a!==i&&g("Expected '"+a+"' instead of '"+i+"'");i=p.charAt(e);e+=1;return i}function g(a){a=new SyntaxError(a);a.zd=e;a.text=p;throw a;}var e,i,k={'"':'"',"\\":"\\","/":"/",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"},p,s;s=function(){b();switch(i){case "{":var e;a:{var v,k={};if("{"===i){h("{");b();if("}"===i){h("}");e=k;break a}for(;i;){v=c();b();h(":");Object.hasOwnProperty.call(k,v)&&g('Duplicate key "'+v+'"');k[v]=s();b();if("}"===i){h("}");e=k;break a}h(",");b()}}g("Bad object")}return e;case "[":a:{e= -[];if("["===i){h("[");b();if("]"===i){h("]");v=e;break a}for(;i;){e.push(s());b();if("]"===i){h("]");v=e;break a}h(",");b()}}g("Bad array")}return v;case '"':return c();case "-":return f();default:return"0"<=i&&"9">=i?f():a()}};return function(a){p=a;e=0;i=" ";a=s();b();i&&g("Syntax error");return a}}();c.yc=function(a){var b,d,f,h,g=0,e=0,i="",i=[];if(!a)return a;a=c.xd(a);do b=a.charCodeAt(g++),d=a.charCodeAt(g++),f=a.charCodeAt(g++),h=b<<16|d<<8|f,b=h>>18&63,d=h>>12&63,f=h>>6&63,h&=63,i[e++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b)+ -"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(d)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(h);while(ge?f++:i=127e?String.fromCharCode(e>>6|192,e&63|128):String.fromCharCode(e>>12|224,e>>6&63|128,e&63|128);i!==r&&(f>c&&(b+=a.substring(c,f)),b+=i,c=f=g+1)}f>c&&(b+=a.substring(c,a.length));return b};c.ib=function(){function a(){function a(b,c){var d,f=0;for(d=0;da?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())};c.fa=function(a){var b= +{};c.a(a,function(a,f){c.Xa(a)&&0=i;)h()}function d(){var a,b,d="",c;if('"'===i)for(;h();){if('"'===i)return h(),d;if("\\"===i)if(h(),"u"===i){for(b=c=0;4>b;b+=1){a=parseInt(h(),16);if(!isFinite(a))break;c=16*c+a}d+=String.fromCharCode(c)}else if("string"===typeof k[i])d+=k[i];else break;else d+=i}g("Bad string")}function c(){var a;a="";"-"===i&&(a="-",h("-"));for(;"0"<=i&&"9">=i;)a+=i,h();if("."===i)for(a+=".";h()&&"0"<=i&&"9">=i;)a+=i;if("e"===i||"E"===i){a+=i;h();if("-"===i||"+"===i)a+=i,h();for(;"0"<=i&&"9">=i;)a+=i,h()}a= ++a;if(isFinite(a))return a;g("Bad number")}function h(a){a&&a!==i&&g("Expected '"+a+"' instead of '"+i+"'");i=p.charAt(e);e+=1;return i}function g(a){a=new SyntaxError(a);a.Cd=e;a.text=p;throw a;}var e,i,k={'"':'"',"\\":"\\","/":"/",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"},p,s;s=function(){b();switch(i){case "{":var e;a:{var t,k={};if("{"===i){h("{");b();if("}"===i){h("}");e=k;break a}for(;i;){t=d();b();h(":");Object.hasOwnProperty.call(k,t)&&g('Duplicate key "'+t+'"');k[t]=s();b();if("}"===i){h("}"); +e=k;break a}h(",");b()}}g("Bad object")}return e;case "[":a:{e=[];if("["===i){h("[");b();if("]"===i){h("]");t=e;break a}for(;i;){e.push(s());b();if("]"===i){h("]");t=e;break a}h(",");b()}}g("Bad array")}return t;case '"':return d();case "-":return c();default:return"0"<=i&&"9">=i?c():a()}};return function(a){p=a;e=0;i=" ";a=s();b();i&&g("Syntax error");return a}}();c.Bc=function(a){var b,d,f,h,g=0,e=0,i="",i=[];if(!a)return a;a=c.Ad(a);do b=a.charCodeAt(g++),d=a.charCodeAt(g++),f=a.charCodeAt(g++), +h=b<<16|d<<8|f,b=h>>18&63,d=h>>12&63,f=h>>6&63,h&=63,i[e++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(d)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(h);while(ge?c++:i=127e?String.fromCharCode(e>>6|192,e&63|128):String.fromCharCode(e>>12|224,e>>6&63|128,e&63|128);i!==r&&(c>d&&(b+=a.substring(d,c)),b+=i,d=c=g+1)}c>d&&(b+=a.substring(d,a.length));return b};c.jb=function(){function a(){function a(b,c){var d,f=0;for(d=0;dB?(Qa.error("Timeout waiting for mutex on "+s+"; clearing lock. ["+k+"]"),j.removeItem(o),j.removeItem(q),h()):setTimeout(function(){try{a()}catch(c){b&&b(c)}},w*(Math.random()+0.1))}!c&&"function"!==typeof b&&(c=b,b=r);var k=c|| -(new Date).getTime()+"|"+Math.random(),p=(new Date).getTime(),s=this.N,w=this.Rb,B=this.ec,j=this.j,n=s+":X",q=s+":Y",o=s+":Z";try{if(U(j,m))h();else throw Error("localStorage support check failed");}catch(t){b&&b(t)}};var pa=ga("batch");G.prototype.Na=function(a,b,d){var f={id:ea(),flushAfter:(new Date).getTime()+2*b,payload:a};this.Xa.hb(c.bind(function(){var b;try{var c=this.da();c.push(f);(b=this.$a(c))&&this.H.push(f)}catch(e){this.h("Error enqueueing item",a),b=D}d&&d(b)},this),c.bind(function(a){this.h("Error acquiring storage lock", -a);d&&d(D)},this),this.ua)};G.prototype.Ec=function(a){var b=this.H.slice(0,a);if(b.lengthg.flushAfter&&!f[g.id]&&(g.Rc=m,b.push(g),b.length>=a))break}}}return b};G.prototype.Tc=function(a,b){var d={};c.a(a,function(a){d[a]=m});this.H=oa(this.H,d);var f=c.bind(function(){var b;try{var c=this.da(),c=oa(c,d);if(b=this.$a(c))for(var c=this.da(),f=0;f -e.length)this.ea();else{this.Vb=m;var i=c.bind(function(e){this.Vb=D;try{var g=D;if(a.ic)this.ba.vd(v);else if(c.e(e)&&"timeout"===e.error&&(new Date).getTime()-d>=b)this.h("Network timeout; retrying"),this.flush();else if(c.e(e)&&e.R&&(500<=e.R.status||429===e.R.status||"timeout"===e.error)){var i=2*this.pa,k=e.R.responseHeaders;if(k){var j=k["Retry-After"];j&&(i=1E3*parseInt(j,10)||i)}i=Math.min(6E5,i);this.h("Error; retry in "+i+" ms");this.Xb(i)}else if(c.e(e)&&e.R&&413===e.R.status)if(1B?(Ra.error("Timeout waiting for mutex on "+s+"; clearing lock. ["+k+"]"),j.removeItem(n),j.removeItem(q),h()):setTimeout(function(){try{a()}catch(c){b&&b(c)}},w*(Math.random()+0.1))}!c&&"function"!==typeof b&&(c=b,b=r);var k=c|| +(new Date).getTime()+"|"+Math.random(),p=(new Date).getTime(),s=this.P,w=this.Tb,B=this.hc,j=this.j,o=s+":X",q=s+":Y",n=s+":Z";try{if(U(j,m))h();else throw Error("localStorage support check failed");}catch(v){b&&b(v)}};var qa=ga("batch");G.prototype.Na=function(a,b,d){var f={id:ea(),flushAfter:(new Date).getTime()+2*b,payload:a};this.z?this.Ya.ib(c.bind(function(){var b;try{var c=this.ea();c.push(f);(b=this.ab(c))&&this.D.push(f)}catch(e){this.h("Error enqueueing item",a),b=D}d&&d(b)},this),c.bind(function(a){this.h("Error acquiring storage lock", +a);d&&d(D)},this),this.ua):(this.D.push(f),d&&d(m))};G.prototype.Hc=function(a){var b=this.D.slice(0,a);if(this.z&&b.lengthg.flushAfter&&!f[g.id]&&(g.Uc=m,b.push(g),b.length>=a))break}}}return b};G.prototype.Wc=function(a,b){var d={};c.a(a,function(a){d[a]=m});this.D=pa(this.D,d);if(this.z){var f=c.bind(function(){var b;try{var c=this.ea(),c=pa(c,d);if(b=this.ab(c))for(var c= +this.ea(),f=0;ft.length)this.N();else{this.Xb=m;var k=c.bind(function(k){this.Xb=D;try{var t=D;if(a.lc)this.ca.yd(i);else if(c.e(k)&&"timeout"===k.error&&(new Date).getTime()-d>=b)this.h("Network timeout; retrying"),this.flush();else if(c.e(k)&&(500<=k.Sa||429===k.Sa||"timeout"===k.error)){var j=2*this.pa;k.Zb&&(j=1E3*parseInt(k.Zb,10)||j);j=Math.min(6E5,j);this.h("Error; retry in "+j+" ms");this.$b(j)}else if(c.e(k)&& +413===k.Sa)if(1=o.timeout?"timeout":"Bad HTTP status: "+o.status+" "+o.statusText,p.l(a),f&&(k?f({status:0,error:a,R:o}):f(0))};o.send(j)}catch(y){p.l(y), -e=D}else j=t.createElement("script"),j.type="text/javascript",j.async=m,j.defer=m,j.src=a,u=t.getElementsByTagName("script")[0],u.parentNode.insertBefore(j,u);return e};e.prototype.Aa=function(a){function b(a,b){c.a(a,function(a){if(c.isArray(a[0])){var d=b;c.a(a,function(a){d=d[a[0]].apply(d,a.slice(1))})}else this[a[0]].apply(this,a.slice(1))},b)}var d,e=[],h=[],g=[];c.a(a,function(a){a&&(d=a[0],c.isArray(d)?g.push(a):"function"===typeof a?a.call(this):c.isArray(a)&&"alias"===d?e.push(a):c.isArray(a)&& --1!==d.indexOf("track")&&"function"===typeof this[d]?g.push(a):h.push(a))},this);b(e,this);b(h,this);b(g,this)};e.prototype.rb=function(){return!!this.u.K};e.prototype.Cb=function(){var a="__mpq_"+this.c("token"),b=this.c("api_routes");return this.jb=this.jb||{K:{type:"events",F:"/"+b.track,ca:a+"_ev"},Za:{type:"people",F:"/"+b.engage,ca:a+"_pp"},Ra:{type:"groups",F:"/"+b.groups,ca:a+"_gr"}}};e.prototype.Kc=function(){if(!this.rb()){var a=c.bind(function(a){return new C(a.ca,{B:this.config,$c:c.bind(function(b, -c,e){this.k(this.c("api_host")+a.F,this.lb(b),c,this.nb(e,b))},this),la:c.bind(function(b){return this.pb("before_send_"+a.type,b)},this),J:this.c("error_reporter"),kd:c.bind(this.bb,this)})},this),b=this.Cb();this.u={K:a(b.K),Za:a(b.Za),Ra:a(b.Ra)}}this.c("batch_autostart")&&this.ab()};e.prototype.ab=function(){this.oc=m;if(this.rb())this.T=m,c.a(this.u,function(a){a.start()})};e.prototype.bb=function(){this.T=D;c.a(this.u,function(a){a.stop();a.clear()})};e.prototype.push=function(a){this.Aa([a])}; -e.prototype.disable=function(a){"undefined"===typeof a?this.U.Bc=m:this.xa=this.xa.concat(a)};e.prototype.lb=function(a){a=c.ha(a);"base64"===this.c("api_payload_format")&&(a=c.yc(a));return{data:a}};e.prototype.Fa=function(a,b){var d=c.truncate(a.data,255),e=a.F,h=a.Ha,g=a.hd,j=a.ad||{},b=b||P,i=m,k=c.bind(function(){j.cc||(d=this.pb("before_send_"+a.type,d));return d?(o.log("MIXPANEL REQUEST:"),o.log(d),this.k(e,this.lb(d),j,this.nb(b,d))):r},this);this.T&&!g?h.Na(d,function(a){a?b(1,d):k()}):i= -k();return i&&d};e.prototype.o=K(function(a,b,d,e){!e&&"function"===typeof d&&(e=d,d=r);var d=d||{},h=d.transport;if(h)d.fb=h;h=d.send_immediately;"function"!==typeof e&&(e=P);if(c.g(a))this.l("No event name provided to mixpanel.track");else if(this.mb(a))e(0);else{b=c.extend({},b);b.token=this.c("token");var g=this.persistence.Uc(a);c.g(g)||(b.$duration=parseFloat((((new Date).getTime()-g)/1E3).toFixed(3)));this.qb();g=this.c("track_marketing")?c.info.Oc():{};b=c.extend({},c.info.aa({mp_loader:this.c("mp_loader")}), -g,this.persistence.aa(),this.P,this.Db(),b);g=this.c("property_blacklist");c.isArray(g)?c.a(g,function(a){delete b[a]}):this.l("Invalid value for property_blacklist config: "+g);return this.Fa({type:"events",data:{event:a,properties:b},F:this.c("api_host")+"/"+this.c("api_routes").track,Ha:this.u.K,hd:h,ad:d},e)}});e.prototype.fd=K(function(a,b,d){c.isArray(b)||(b=[b]);var e={};e[a]=b;this.m(e);return this.people.set(a,b,d)});e.prototype.vc=K(function(a,b,c){var e=this.s(a),h={};e===l?(h[a]=[b],this.m(h)): --1===e.indexOf(b)&&(e.push(b),h[a]=e,this.m(h));return this.people.ga(a,b,c)});e.prototype.Vc=K(function(a,b,c){var e=this.s(a);if(e!==l){var h=e.indexOf(b);-1(x.__SV||0)?o.A("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."): -(c.a(x._i,function(a){a&&c.isArray(a)&&(F[a[a.length-1]]=S.apply(this,a))}),Ba(),x.init(),c.a(F,function(a){a.ka()}),Aa())})()})(); +record_collect_fonts:D,record_idle_timeout_ms:18E5,record_inline_images:D,record_mask_text_class:/^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$/,record_mask_text_selector:"*",record_max_ms:864E5,record_sessions_percent:0,recorder_src:"https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js"},la=D;e.prototype.Va=function(a,b,d){if(c.g(d))this.l("You must name your new library: init(token, config, name)");else if("mixpanel"===d)this.l("You must initialize the main mixpanel object right after you include the Mixpanel js snippet"); +else return a=S(a,b,d),x[d]=a,a.ka(),a};e.prototype.Y=function(a,b,d){b=b||{};this.__loaded=m;this.config={};var f={};"api_payload_format"in b||(b.api_host||Aa.api_host).match(/\.mixpanel\.com/)&&(f.api_payload_format="json");this.cc(c.extend({},Aa,f,b,{name:d,token:a,callback_fn:("mixpanel"===d?d:"mixpanel."+d)+"._jsc"}));this._jsc=P;this.ya=[];this.za=[];this.xa=[];this.V={disable_all_events:D,identify_called:D};this.u={};if(this.U=this.c("batch_requests"))if(!c.localStorage.sa(m)||!O)this.U=D, +n.log("Turning off Mixpanel request-queueing; needs XHR and localStorage support"),c.a(this.Eb(),function(a){n.log("Clearing batch queue "+a.da);c.localStorage.remove(a.da)});else if(this.Nc(),ba&&o.addEventListener){var e=c.bind(function(){this.u.L.va||this.u.L.flush({lc:m})},this);o.addEventListener("pagehide",function(a){a.persisted&&e()});o.addEventListener("visibilitychange",function(){"hidden"===v.visibilityState&&e()})}this.persistence=this.cookie=new q(this.config);this.R={};this.wc();a=c.jb(); +this.M()||this.w({distinct_id:"$device:"+a,$device_id:a},"");(a=this.c("track_pageview"))&&this.xc(a);0=n.timeout?"timeout":"Bad HTTP status: "+n.status+" "+n.statusText,p.l(a),f&&(k?f({status:0,Sa:n.status,error:a,Zb:(n.responseHeaders|| +{})["Retry-After"]}):f(0))};n.send(j)}catch(y){p.l(y),e=D}else j=v.createElement("script"),j.type="text/javascript",j.async=m,j.defer=m,j.src=a,u=v.getElementsByTagName("script")[0],u.parentNode.insertBefore(j,u);return e};e.prototype.Aa=function(a){function b(a,b){c.a(a,function(a){if(c.isArray(a[0])){var d=b;c.a(a,function(a){d=d[a[0]].apply(d,a.slice(1))})}else this[a[0]].apply(this,a.slice(1))},b)}var d,e=[],h=[],g=[];c.a(a,function(a){a&&(d=a[0],c.isArray(d)?g.push(a):"function"===typeof a?a.call(this): +c.isArray(a)&&"alias"===d?e.push(a):c.isArray(a)&&-1!==d.indexOf("track")&&"function"===typeof this[d]?g.push(a):h.push(a))},this);b(e,this);b(h,this);b(g,this)};e.prototype.sb=function(){return!!this.u.L};e.prototype.Eb=function(){var a="__mpq_"+this.c("token"),b=this.c("api_routes");return this.kb=this.kb||{L:{type:"events",H:"/"+b.track,da:a+"_ev"},$a:{type:"people",H:"/"+b.engage,da:a+"_pp"},Ra:{type:"groups",H:"/"+b.groups,da:a+"_gr"}}};e.prototype.Nc=function(){if(!this.sb()){var a=c.bind(function(a){return new C(a.da, +{C:this.config,K:this.c("error_reporter"),cd:c.bind(function(b,c,e){this.k(this.c("api_host")+a.H,this.mb(b),c,this.ob(e,b))},this),la:c.bind(function(b){return this.qb("before_send_"+a.type,b)},this),nd:c.bind(this.cb,this),z:m})},this),b=this.Eb();this.u={L:a(b.L),$a:a(b.$a),Ra:a(b.Ra)}}this.c("batch_autostart")&&this.bb()};e.prototype.bb=function(){this.rc=m;if(this.sb())this.U=m,c.a(this.u,function(a){a.start()})};e.prototype.cb=function(){this.U=D;c.a(this.u,function(a){a.stop();a.clear()})}; +e.prototype.push=function(a){this.Aa([a])};e.prototype.disable=function(a){"undefined"===typeof a?this.V.Ec=m:this.xa=this.xa.concat(a)};e.prototype.mb=function(a){a=c.ha(a);"base64"===this.c("api_payload_format")&&(a=c.Bc(a));return{data:a}};e.prototype.Fa=function(a,b){var d=c.truncate(a.data,255),e=a.H,h=a.Ha,g=a.ld,j=a.dd||{},b=b||P,i=m,k=c.bind(function(){j.fc||(d=this.qb("before_send_"+a.type,d));return d?(n.log("MIXPANEL REQUEST:"),n.log(d),this.k(e,this.mb(d),j,this.ob(b,d))):r},this);this.U&& +!g?h.Na(d,function(a){a?b(1,d):k()}):i=k();return i&&d};e.prototype.o=K(function(a,b,d,e){!e&&"function"===typeof d&&(e=d,d=r);var d=d||{},h=d.transport;if(h)d.gb=h;h=d.send_immediately;"function"!==typeof e&&(e=P);if(c.g(a))this.l("No event name provided to mixpanel.track");else if(this.nb(a))e(0);else{b=c.extend({},b);b.token=this.c("token");var g=this.persistence.Xc(a);c.g(g)||(b.$duration=parseFloat((((new Date).getTime()-g)/1E3).toFixed(3)));this.rb();g=this.c("track_marketing")?c.info.Rc(): +{};b=c.extend({},c.info.ba({mp_loader:this.c("mp_loader")}),g,this.persistence.ba(),this.R,this.Fb(),b);g=this.c("property_blacklist");c.isArray(g)?c.a(g,function(a){delete b[a]}):this.l("Invalid value for property_blacklist config: "+g);return this.Fa({type:"events",data:{event:a,properties:b},H:this.c("api_host")+"/"+this.c("api_routes").track,Ha:this.u.L,ld:h,dd:d},e)}});e.prototype.jd=K(function(a,b,d){c.isArray(b)||(b=[b]);var e={};e[a]=b;this.m(e);return this.people.set(a,b,d)});e.prototype.yc= +K(function(a,b,c){var e=this.s(a),h={};e===l?(h[a]=[b],this.m(h)):-1===e.indexOf(b)&&(e.push(b),h[a]=e,this.m(h));return this.people.ga(a,b,c)});e.prototype.Yc=K(function(a,b,c){var e=this.s(a);if(e!==l){var h=e.indexOf(b);-1(x.__SV||0)?n.B("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."):(c.a(x._i,function(a){a&&c.isArray(a)&&(F[a[a.length-1]]=S.apply(this,a))}),Ca(),x.init(),c.a(F,function(a){a.ka()}),Ba())})(function(a,b){var c=document.createElement("script");c.type="text/javascript";c.async=m;c.onload=b;c.src=a;document.head.appendChild(c)})})(); })(); diff --git a/dist/mixpanel.umd.js b/dist/mixpanel.umd.js index f8a611b1..007ff2e2 100644 --- a/dist/mixpanel.umd.js +++ b/dist/mixpanel.umd.js @@ -4,9 +4,4516 @@ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.mixpanel = factory()); })(this, (function () { 'use strict'; + var NodeType; + (function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; + })(NodeType || (NodeType = {})); + + function isElement(n) { + return n.nodeType === n.ELEMENT_NODE; + } + function isShadowRoot(n) { + const host = n === null || n === void 0 ? void 0 : n.host; + return Boolean((host === null || host === void 0 ? void 0 : host.shadowRoot) === n); + } + function isNativeShadowDom(shadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; + } + function fixBrowserCompatibilityIssuesInCSS(cssText) { + if (cssText.includes(' background-clip: text;') && + !cssText.includes(' -webkit-background-clip: text;')) { + cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;'); + } + return cssText; + } + function escapeImportStatement(rule) { + const { cssText } = rule; + if (cssText.split('"').length < 3) + return cssText; + const statement = ['@import', `url(${JSON.stringify(rule.href)})`]; + if (rule.layerName === '') { + statement.push(`layer`); + } + else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); + } + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); + } + if (rule.media.length) { + statement.push(rule.media.mediaText); + } + return statement.join(' ') + ';'; + } + function stringifyStylesheet(s) { + try { + const rules = s.rules || s.cssRules; + return rules + ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) + : null; + } + catch (error) { + return null; + } + } + function stringifyRule(rule) { + let importStringified; + if (isCSSImportRule(rule)) { + try { + importStringified = + stringifyStylesheet(rule.styleSheet) || + escapeImportStatement(rule); + } + catch (error) { + } + } + else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + return fixSafariColons(rule.cssText); + } + return importStringified || rule.cssText; + } + function fixSafariColons(cssStringified) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); + } + function isCSSImportRule(rule) { + return 'styleSheet' in rule; + } + function isCSSStyleRule(rule) { + return 'selectorText' in rule; + } + class Mirror { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + } + function createMirror() { + return new Mirror(); + } + function maskInputValue({ element, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + const actualType = type && toLowerCase(type); + if (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType])) { + if (maskInputFn) { + text = maskInputFn(text, element); + } + else { + text = '*'.repeat(text.length); + } + } + return text; + } + function toLowerCase(str) { + return str.toLowerCase(); + } + const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; + function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; + } + function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? + toLowerCase(type) + : null; + } + function extractFileExtension(path, baseURL) { + var _a; + let url; + try { + url = new URL(path, baseURL !== null && baseURL !== void 0 ? baseURL : window.location.href); + } + catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; + const match = url.pathname.match(regex); + return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null; + } + + let _id = 1; + const tagNameRegex = new RegExp('[^a-z0-9-_:]'); + const IGNORED_NODE = -2; + function genId() { + return _id++; + } + function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; + } + function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; + } + let canvasService; + let canvasCtx; + const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; + const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; + const URL_WWW_MATCH = /^www\..*/i; + const DATA_URI = /^(data:)([^,]*),(.*)/i; + function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); + } + const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; + const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; + function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + const output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); + } + function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; + } + function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); + } + function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; + } + function transformAttribute(doc, tagName, name, value) { + if (!value) { + return value; + } + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + return value; + } + function ignoreAttribute(tagName, name, _value) { + return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; + } + function _isBlockedElement(element, blockClass, blockSelector) { + try { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = element.classList.length; eIndex--;) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + } + catch (e) { + } + return false; + } + function classMatchesRegex(node, regex, checkAncestors) { + if (!node) + return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + for (let eIndex = node.classList.length; eIndex--;) { + const className = node.classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + function needMaskingText(node, maskTextClass, maskTextSelector, checkAncestors) { + try { + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + if (el === null) + return false; + if (typeof maskTextClass === 'string') { + if (checkAncestors) { + if (el.closest(`.${maskTextClass}`)) + return true; + } + else { + if (el.classList.contains(maskTextClass)) + return true; + } + } + else { + if (classMatchesRegex(el, maskTextClass, checkAncestors)) + return true; + } + if (maskTextSelector) { + if (checkAncestors) { + if (el.closest(maskTextSelector)) + return true; + } + else { + if (el.matches(maskTextSelector)) + return true; + } + } + } + catch (e) { + } + return false; + } + function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); + } + iframeEl.addEventListener('load', listener); + } + function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; + } + catch (error) { + return; + } + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + } + function serializeNode(n, options) { + const { doc, mirror, blockClass, blockSelector, needsMask, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options; + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: n.compatMode, + }; + } + else { + return { + type: NodeType.Document, + childNodes: [], + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n, { + needsMask, + maskTextFn, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } + } + function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; + } + function serializeTextNode(n, options) { + var _a; + const { needsMask, maskTextFn, rootId } = options; + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStylesheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (!isStyle && !isScript && textContent && needsMask) { + textContent = maskTextFn + ? maskTextFn(textContent, n.parentElement) + : textContent.replace(/[\S]/g, '*'); + } + return { + type: NodeType.Text, + textContent: textContent || '', + isStyle, + rootId, + }; + } + function serializeElementNode(n, options) { + const { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions = {}, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, } = options; + const needBlock = _isBlockedElement(n, blockClass, blockSelector); + const tagName = getValidTagName(n); + let attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = stringifyStylesheet(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = n.value; + const checked = n.checked; + if (attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value) { + attributes.value = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } + else if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + image.removeEventListener('load', recordInlineImage); + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.addEventListener('load', recordInlineImage); + } + if (tagName === 'audio' || tagName === 'video') { + const mediaAttributes = attributes; + mediaAttributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + mediaAttributes.rr_mediaCurrentTime = n.currentTime; + mediaAttributes.rr_mediaPlaybackRate = n.playbackRate; + mediaAttributes.rr_mediaMuted = n.muted; + mediaAttributes.rr_mediaLoop = n.loop; + mediaAttributes.rr_mediaVolume = n.volume; + } + if (!newlyAddedElement) { + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + let isCustomElement; + try { + if (customElements.get(tagName)) + isCustomElement = true; + } + catch (e) { + } + return { + type: NodeType.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + isCustom: isCustomElement, + }; + } + function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } + } + function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + return true; + } + else if (sn.type === NodeType.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + extractFileExtension(sn.attributes.href) === 'js'))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; + } + function serializeNodeWithId(n, options) { + const { doc, mirror, blockClass, blockSelector, maskTextClass, maskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options; + let { needsMask } = options; + let { preserveWhiteSpace = true } = options; + if (!needsMask && + n.childNodes) { + const checkAncestors = needsMask === undefined; + needsMask = needMaskingText(n, maskTextClass, maskTextSelector, checkAncestors); + } + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if (mirror.hasNode(n)) { + id = mirror.getId(n); + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + mirror.add(n, serializedNode); + if (id === IGNORED_NODE) { + return null; + } + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + const shadowRoot = n.shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType.Document || + serializedNode.type === NodeType.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }; + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'textarea' && + serializedNode.attributes.value !== undefined) ; + else { + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css'))) { + onceStylesheetLoaded(n, () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedLinkNode) { + onStylesheetLoad(n, serializedLinkNode); + } + } + }, stylesheetLoadTimeout); + } + return serializedNode; + } + function snapshot(n, options) { + const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + }); + } + + function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); + } + const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; + let _mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + }; + if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); + } + function throttle(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (...args) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; + } + function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); + } + function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { + }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { + }; + } + } + let nowTimestamp = Date.now; + if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) { + nowTimestamp = () => new Date().getTime(); + } + function getWindowScroll(win) { + var _a, _b, _c, _d, _e, _f; + const doc = win.document; + return { + left: doc.scrollingElement + ? doc.scrollingElement.scrollLeft + : win.pageXOffset !== undefined + ? win.pageXOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollLeft) || + ((_b = (_a = doc === null || doc === void 0 ? void 0 : doc.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || + ((_c = doc === null || doc === void 0 ? void 0 : doc.body) === null || _c === void 0 ? void 0 : _c.scrollLeft) || + 0, + top: doc.scrollingElement + ? doc.scrollingElement.scrollTop + : win.pageYOffset !== undefined + ? win.pageYOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollTop) || + ((_e = (_d = doc === null || doc === void 0 ? void 0 : doc.body) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.scrollTop) || + ((_f = doc === null || doc === void 0 ? void 0 : doc.body) === null || _f === void 0 ? void 0 : _f.scrollTop) || + 0, + }; + } + function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); + } + function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); + } + function closestElementOfNode(node) { + if (!node) { + return null; + } + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + return el; + } + function isBlocked(node, blockClass, blockSelector, checkAncestors) { + if (!node) { + return false; + } + const el = closestElementOfNode(node); + if (!el) { + return false; + } + try { + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) + return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) + return true; + } + else { + if (classMatchesRegex(el, blockClass, checkAncestors)) + return true; + } + } + catch (e) { + } + if (blockSelector) { + if (el.matches(blockSelector)) + return true; + if (checkAncestors && el.closest(blockSelector) !== null) + return true; + } + return false; + } + function isSerialized(n, mirror) { + return mirror.getId(n) !== -1; + } + function isIgnored(n, mirror) { + return mirror.getId(n) === IGNORED_NODE; + } + function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); + } + function legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); + } + function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = (...args) => { + let node = args[0]; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } + } + function isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); + } + function isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); + } + function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); + } + class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + } + getId(stylesheet) { + var _a; + return (_a = this.styleIDMap.get(stylesheet)) !== null && _a !== void 0 ? _a : -1; + } + has(stylesheet) { + return this.styleIDMap.has(stylesheet); + } + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; + } + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + getStyle(id) { + return this.idStyleMap.get(id) || null; + } + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; + } + generateId() { + return this.id++; + } + } + function getShadowHost(n) { + var _a, _b; + let shadowHost = null; + if (((_b = (_a = n.getRootNode) === null || _a === void 0 ? void 0 : _a.call(n)) === null || _b === void 0 ? void 0 : _b.nodeType) === Node.DOCUMENT_FRAGMENT_NODE && + n.getRootNode().host) + shadowHost = n.getRootNode().host; + return shadowHost; + } + function getRootShadowHost(n) { + let rootShadowHost = n; + let shadowHost; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; + return rootShadowHost; + } + function shadowHostInDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + const shadowHost = getRootShadowHost(n); + return doc.contains(shadowHost); + } + function inDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + return doc.contains(n) || shadowHostInDom(n); + } + + var EventType$1 = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType$1 || {}); + var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource$1 || {}); + var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; + })(MouseInteractions || {}); + var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; + })(PointerTypes || {}); + var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; + })(CanvasContext || {}); + + function isNodeInLinkedList(n) { + return '__ln' in n; + } + class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + this.tail = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + if (node.next === null) { + this.tail = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + else { + this.tail = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } + } + const moveKey = (id, parentId) => `${id}@${parentId}`; + class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addedIds = new Set(); + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + if (!n.parentNode || + !inDom(n) || + n.parentNode.tagName === 'TEXTAREA') { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + let tailNode = addList.tail; + while (tailNode) { + const _node = tailNode; + tailNode = tailNode.previous; + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (nextId === -1) + continue; + else if (parentId !== -1) { + node = _node; + break; + } + else { + const unhandledNode = _node.value; + if (unhandledNode.parentNode && + unhandledNode.parentNode.nodeType === + Node.DOCUMENT_FRAGMENT_NODE) { + const shadowHost = unhandledNode.parentNode + .host; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => { + const n = text.node; + if (n.parentNode && + n.parentNode.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(n.parentNode); + } + return { + id: this.mirror.getId(n), + value: text.value, + }; + }) + .filter((text) => !addedIds.has(text.id)) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => { + const { attributes } = attribute; + if (typeof attributes.style === 'string') { + const diffAsStr = JSON.stringify(attribute.styleDiff); + const unchangedAsStr = JSON.stringify(attribute._unchangedStyles); + if (diffAsStr.length < attributes.style.length) { + if ((diffAsStr + unchangedAsStr).split('var(').length === + attributes.style.split('var(').length) { + attributes.style = attribute.styleDiff; + } + } + } + return { + id: this.mirror.getId(attribute.node), + attributes: attributes, + }; + }) + .filter((attribute) => !addedIds.has(attribute.id)) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.genTextAreaValueMutation = (textarea) => { + let item = this.attributeMap.get(textarea); + if (!item) { + item = { + node: textarea, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(textarea, item); + } + item.attributes.value = Array.from(textarea.childNodes, (cn) => cn.textContent || '').join(''); + }; + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, false) && + value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, true) && value + ? this.maskTextFn + ? this.maskTextFn(value, closestElementOfNode(m.target)) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let attributeName = m.attributeName; + let value = m.target.getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ + element: target, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type, + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + value === m.oldValue) { + return; + } + let item = this.attributeMap.get(m.target); + if (target.tagName === 'IFRAME' && + attributeName === 'src' && + !this.keepIframeSrcFn(value)) { + if (!target.contentDocument) { + attributeName = 'rr_src'; + } + else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(m.target, item); + } + if (attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (!ignoreAttribute(target.tagName, attributeName)) { + item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value); + if (attributeName === 'style') { + if (!this.unattachedDoc) { + try { + this.unattachedDoc = + document.implementation.createHTMLDocument(); + } + catch (e) { + this.unattachedDoc = this.doc; + } + } + const old = this.unattachedDoc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + item.styleDiff[pname] = newValue; + } + else { + item.styleDiff[pname] = [newValue, newPriority]; + } + } + else { + item._unchangedStyles[pname] = [newValue, newPriority]; + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + item.styleDiff[pname] = false; + } + } + } + } + break; + } + case 'childList': { + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; + if (m.target.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(m.target); + return; + } + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (this.processedNodeManager.inOtherBuffer(n, this)) + return; + if (this.addedSet.has(n) || this.movedSet.has(n)) + return; + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); + } + } + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'dataURLOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'processedNodeManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } + } + function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); + } + function isParentRemoved(removes, n, mirror) { + if (removes.length === 0) + return false; + return _isParentRemoved(removes, n, mirror); + } + function _isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); + } + function isAncestorInSet(set, n) { + if (set.size === 0) + return false; + return _isAncestorInSet(set, n); + } + function _isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); + } + + let errorHandler; + function registerErrorHandler(handler) { + errorHandler = handler; + } + function unregisterErrorHandler() { + errorHandler = undefined; + } + const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { + try { + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return; + } + throw error; + } + }); + return rrwebWrapped; + }; + + const mutationBuffers = []; + function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { + } + return event && event.target; + } + function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer))); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; + } + function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { + }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle(callbackWrapper((source) => { + const totalOffset = Date.now() - timeBaseline; + mousemoveCb(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }), callbackThreshold); + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = legacy_isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = nowTimestamp(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: nowTimestamp() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource$1.Drag + : evt instanceof MouseEvent + ? IncrementalSource$1.MouseMove + : IncrementalSource$1.TouchMove); + }), threshold, { + trailing: false, + })); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { + }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + let currentPointerType = null; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, true)) { + return; + } + let pointerType = null; + let thisEventKey = eventKey; + if ('pointerType' in event) { + switch (event.pointerType) { + case 'mouse': + pointerType = PointerTypes.Mouse; + break; + case 'touch': + pointerType = PointerTypes.Touch; + break; + case 'pen': + pointerType = PointerTypes.Pen; + break; + } + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + thisEventKey = 'TouchStart'; + } + else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) { + thisEventKey = 'TouchEnd'; + } + } + else if (pointerType === PointerTypes.Pen) ; + } + else if (legacy_isTouchEvent(event)) { + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + if ((thisEventKey.startsWith('Touch') && + pointerType === PointerTypes.Touch) || + (thisEventKey.startsWith('Mouse') && + pointerType === PointerTypes.Mouse)) { + pointerType = null; + } + } + else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; + } + const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)(Object.assign({ type: MouseInteractions[thisEventKey], id, x: clientX, y: clientY }, (pointerType !== null && { pointerType }))); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + let eventName = toLowerCase(eventKey); + const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + return; + } + } + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }) { + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); + } + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }), sampling.scroll || 100)); + return on('scroll', updatePosition, doc); + } + function initViewportResizeObserver({ viewportResizeCb }, { win }) { + let lastH = -1; + let lastW = -1; + const updateDimension = callbackWrapper(throttle(callbackWrapper(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }), 200)); + return on('resize', updateDimension, win); + } + const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; + const lastInputValueMap = new WeakMap(); + function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + if (target.classList.contains(ignoreClass) || + (ignoreSelector && target.matches(ignoreSelector))) { + return; + } + let text = target.value; + let isChecked = false; + const type = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + else if (maskInputOptions[tagName.toLowerCase()] || + maskInputOptions[type]) { + text = maskInputValue({ + element: target, + maskInputOptions, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, userTriggeredOnInput + ? { text, isChecked, userTriggered } + : { text, isChecked }); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = el.value; + cbWithDedup(el, userTriggeredOnInput + ? { text, isChecked: !isChecked, userTriggered: false } + : { text, isChecked: !isChecked }); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + callbackWrapper(inputCb)(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const currentWindow = doc.defaultView; + if (!currentWindow) { + return () => { + handlers.forEach((h) => h()); + }; + } + const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [currentWindow.HTMLInputElement.prototype, 'value'], + [currentWindow.HTMLInputElement.prototype, 'checked'], + [currentWindow.HTMLSelectElement.prototype, 'value'], + [currentWindow.HTMLTextAreaElement.prototype, 'value'], + [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'], + [currentWindow.HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ + target: this, + isTrusted: false, + }); + }, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); + } + function getIdAndStyleId(sheet, mirror, styleMirror) { + let id, styleId; + if (!sheet) + return {}; + if (sheet.ownerNode) + id = mirror.getId(sheet.ownerNode); + else + styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; + } + function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { + }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + let replace; + if (win.CSSStyleSheet.prototype.replace) { + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = new Proxy(replace, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + let replaceSync; + if (win.CSSStyleSheet.prototype.replaceSync) { + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); + } + function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) { + var _a, _b, _c; + let hostId = null; + if (host.nodeName === '#document') + hostId = mirror.getId(host); + else + hostId = mirror.getId(host.host); + const patchTarget = host.nodeName === '#document' + ? (_a = host.defaultView) === null || _a === void 0 ? void 0 : _a.Document + : (_c = (_b = host.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.ShadowRoot; + const originalPropertyDescriptor = (patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype) + ? Object.getOwnPropertyDescriptor(patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype, 'adoptedStyleSheets') + : undefined; + if (hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor) + return () => { + }; + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get() { + var _a; + return (_a = originalPropertyDescriptor.get) === null || _a === void 0 ? void 0 : _a.call(this); + }, + set(sheets) { + var _a; + const result = (_a = originalPropertyDescriptor.set) === null || _a === void 0 ? void 0 : _a.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } + catch (e) { + } + } + return result; + }, + }); + return callbackWrapper(() => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get: originalPropertyDescriptor.get, + set: originalPropertyDescriptor.set, + }); + }); + } + function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property, value, priority] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return setProperty.apply(thisArg, [property, value, priority]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return removeProperty.apply(thisArg, [property]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); + } + function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, mirror, sampling, doc, }) { + const handler = callbackWrapper((type) => throttle(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const { currentTime, volume, muted, playbackRate, loop } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + playbackRate, + loop, + }); + }), sampling.media || 500)); + const handlers = [ + on('play', handler(0), doc), + on('pause', handler(1), doc), + on('seeked', handler(2), doc), + on('volumechange', handler(3), doc), + on('ratechange', handler(4), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { + }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(callbackWrapper(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }), 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initSelectionObserver(param) { + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; + let collapsed = true; + const updateSelection = callbackWrapper(() => { + const selection = doc.getSelection(); + if (!selection || (collapsed && (selection === null || selection === void 0 ? void 0 : selection.isCollapsed))) + return; + collapsed = selection.isCollapsed || false; + const ranges = []; + const count = selection.rangeCount || 0; + for (let i = 0; i < count; i++) { + const range = selection.getRangeAt(i); + const { startContainer, startOffset, endContainer, endOffset } = range; + const blocked = isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); + if (blocked) + continue; + ranges.push({ + start: mirror.getId(startContainer), + startOffset, + end: mirror.getId(endContainer), + endOffset, + }); + } + selectionCb({ ranges }); + }); + updateSelection(); + return on('selectionchange', updateSelection); + } + function initCustomElementObserver({ doc, customElementCb, }) { + const win = doc.defaultView; + if (!win || !win.customElements) + return () => { }; + const restoreHandler = patch(win.customElements, 'define', function (original) { + return function (name, constructor, options) { + try { + customElementCb({ + define: { + name, + }, + }); + } + catch (e) { + console.warn(`Custom element callback failed for ${name}`); + } + return original.apply(this, [name, constructor, options]); + }; + }); + return restoreHandler; + } + function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, selectionCb, customElementCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; + o.selectionCb = (...p) => { + if (hooks.selection) { + hooks.selection(...p); + } + selectionCb(...p); + }; + o.customElementCb = (...c) => { + if (hooks.customElement) { + hooks.customElement(...c); + } + customElementCb(...c); + }; + } + function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { + }; + } + mergeHooks(o, hooks); + let mutationObserver; + if (o.recordDOM) { + mutationObserver = initMutationObserver(o, o.doc); + } + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o, { + win: currentWindow, + }); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + let styleSheetObserver = () => { }; + let adoptedStyleSheetObserver = () => { }; + let styleDeclarationObserver = () => { }; + let fontObserver = () => { }; + if (o.recordDOM) { + styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); + styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + if (o.collectFonts) { + fontObserver = initFontObserver(o); + } + } + const selectionObserver = initSelectionObserver(o); + const customElementObserver = initCustomElementObserver(o); + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver === null || mutationObserver === void 0 ? void 0 : mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + adoptedStyleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + selectionObserver(); + customElementObserver(); + pluginHandlers.forEach((h) => h()); + }); + } + function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; + } + function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); + } + + class CrossOriginIframeMirror { + constructor(generateIdFn) { + this.generateIdFn = generateIdFn; + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + } + getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) { + const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe); + let id = idToRemoteIdMap.get(remoteId); + if (!id) { + id = this.generateIdFn(); + idToRemoteIdMap.set(remoteId, id); + remoteIdToIdMap.set(id, remoteId); + } + return id; + } + getIds(iframe, remoteId) { + const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + } + getRemoteId(iframe, id, map) { + const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe); + if (typeof id !== 'number') + return id; + const remoteId = remoteIdToIdMap.get(id); + if (!remoteId) + return -1; + return remoteId; + } + getRemoteIds(iframe, ids) { + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + } + reset(iframe) { + if (!iframe) { + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + return; + } + this.iframeIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToIdMap.delete(iframe); + } + getIdToRemoteIdMap(iframe) { + let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); + if (!idToRemoteIdMap) { + idToRemoteIdMap = new Map(); + this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + } + return idToRemoteIdMap; + } + getRemoteIdToIdMap(iframe) { + let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); + if (!remoteIdToIdMap) { + remoteIdToIdMap = new Map(); + this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + } + return remoteIdToIdMap; + } + } + + class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.crossOriginIframeMap = new WeakMap(); + this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + this.crossOriginIframeRootIdMap = new WeakMap(); + this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; + this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)); + this.mirror = options.mirror; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + if (iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0) + this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument)); + } + handleMessage(message) { + const crossOriginMessageEvent = message; + if (crossOriginMessageEvent.data.type !== 'rrweb' || + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin) + return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) + return; + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) + return; + const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event); + if (transformedEvent) + this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout); + } + transformCrossOriginEvent(iframeEl, e) { + var _a; + switch (e.type) { + case EventType$1.FullSnapshot: { + this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); + this.replaceIdOnNode(e.data.node, iframeEl); + const rootId = e.data.node.id; + this.crossOriginIframeRootIdMap.set(iframeEl, rootId); + this.patchRootIdOnNode(e.data.node, rootId); + return { + timestamp: e.timestamp, + type: EventType$1.IncrementalSnapshot, + data: { + source: IncrementalSource$1.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + case EventType$1.Meta: + case EventType$1.Load: + case EventType$1.DomContentLoaded: { + return false; + } + case EventType$1.Plugin: { + return e; + } + case EventType$1.Custom: { + this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); + return e; + } + case EventType$1.IncrementalSnapshot: { + switch (e.data.source) { + case IncrementalSource$1.Mutation: { + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, [ + 'parentId', + 'nextId', + 'previousId', + ]); + this.replaceIdOnNode(n.node, iframeEl); + const rootId = this.crossOriginIframeRootIdMap.get(iframeEl); + rootId && this.patchRootIdOnNode(n.node, rootId); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.Drag: + case IncrementalSource$1.TouchMove: + case IncrementalSource$1.MouseMove: { + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.ViewportResize: { + return false; + } + case IncrementalSource$1.MediaInteraction: + case IncrementalSource$1.MouseInteraction: + case IncrementalSource$1.Scroll: + case IncrementalSource$1.CanvasMutation: + case IncrementalSource$1.Input: { + this.replaceIds(e.data, iframeEl, ['id']); + return e; + } + case IncrementalSource$1.StyleSheetRule: + case IncrementalSource$1.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleId']); + return e; + } + case IncrementalSource$1.Font: { + return e; + } + case IncrementalSource$1.Selection: { + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); + return e; + } + case IncrementalSource$1.AdoptedStyleSheet: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); + return e; + } + } + } + } + return false; + } + replace(iframeMirror, obj, iframeEl, keys) { + for (const key of keys) { + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') + continue; + if (Array.isArray(obj[key])) { + obj[key] = iframeMirror.getIds(iframeEl, obj[key]); + } + else { + obj[key] = iframeMirror.getId(iframeEl, obj[key]); + } + } + return obj; + } + replaceIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); + } + replaceStyleIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); + } + replaceIdOnNode(node, iframeEl) { + this.replaceIds(node, iframeEl, ['id', 'rootId']); + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } + patchRootIdOnNode(node, rootId) { + if (node.type !== NodeType.Document && !node.rootId) + node.rootId = rootId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.patchRootIdOnNode(child, rootId); + }); + } + } + } + + class ShadowDomManager { + constructor(options) { + this.shadowDoms = new WeakSet(); + this.restoreHandlers = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + this.init(); + } + init() { + this.reset(); + this.patchAttachShadow(Element, document); + } + addShadowRoot(shadowRoot, doc) { + if (!isNativeShadowDom(shadowRoot)) + return; + if (this.shadowDoms.has(shadowRoot)) + return; + this.shadowDoms.add(shadowRoot); + const observer = initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + this.restoreHandlers.push(() => observer.disconnect()); + this.restoreHandlers.push(initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror }))); + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0) + this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host)); + this.restoreHandlers.push(initAdoptedStyleSheetObserver({ + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, shadowRoot)); + }, 0); + } + observeAttachShadow(iframeElement) { + if (!iframeElement.contentWindow || !iframeElement.contentDocument) + return; + this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument); + } + patchAttachShadow(element, doc) { + const manager = this; + this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) { + return function (option) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot && inDom(this)) + manager.addShadowRoot(this.shadowRoot, doc); + return shadowRoot; + }; + })); + } + reset() { + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } + catch (e) { + } + }); + this.restoreHandlers = []; + this.shadowDoms = new WeakSet(); + } + } + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; + } + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const canvasVarMap = new Map(); + function variableListFor(ctx, ctor) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); + } + const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; + }; + function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + const src = value.toDataURL(); + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; + } + const serializeArgs = (args, win, ctx) => { + return args.map((arg) => serializeArg(arg, win, ctx)); + }; + const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); + }; + + function initCanvas2DMutationObserver(cb, win, blockClass, blockSelector) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { + setTimeout(() => { + const recordArgs = serializeArgs(args, win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function getNormalizedContextName(contextType) { + return contextType === 'experimental-webgl' ? 'webgl' : contextType; + } + function initCanvasContextObserver(win, blockClass, blockSelector, setPreserveDrawingBufferToTrue) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, true)) { + const ctxName = getNormalizedContextName(contextType); + if (!('__context' in this)) + this.__context = ctxName; + if (setPreserveDrawingBufferToTrue && + ['webgl', 'webgl2'].includes(ctxName)) { + if (args[0] && typeof args[0] === 'object') { + const contextAttributes = args[0]; + if (!contextAttributes.preserveDrawingBuffer) { + contextAttributes.preserveDrawingBuffer = true; + } + } + else { + args.splice(0, 1, { + preserveDrawingBuffer: true, + }); + } + } + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function patchGLPrototype(prototype, type, cb, blockClass, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + if ([ + 'isContextLost', + 'canvas', + 'drawingBufferWidth', + 'drawingBufferHeight', + ].includes(prop)) { + continue; + } + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, this); + if ('tagName' in this.canvas && + !isBlocked(this.canvas, blockClass, blockSelector, true)) { + const recordArgs = serializeArgs(args, win, this); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; + } + function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function funcToSource(fn, sourcemapArg) { + var sourcemap = sourcemapArg === undefined ? null : sourcemapArg; + var source = fn.toString(); + var lines = source.split('\n'); + lines.pop(); + lines.shift(); + var blankPrefixLength = lines[0].search(/\S/); + var regex = /(['"])__worker_loader_strict__(['"])/g; + for (var i = 0, n = lines.length; i < n; ++i) { + lines[i] = lines[i].substring(blankPrefixLength).replace(regex, '$1use strict$2') + '\n'; + } + if (sourcemap) { + lines.push('\/\/# sourceMappingURL=' + sourcemap + '\n'); + } + return lines; + } + + function createURL(fn, sourcemapArg) { + var lines = funcToSource(fn, sourcemapArg); + var blob = new Blob(lines, { type: 'application/javascript' }); + return URL.createObjectURL(blob); + } + + function createInlineWorkerFactory(fn, sourcemapArg) { + var url; + return function WorkerFactory(options) { + url = url || createURL(fn, sourcemapArg); + return new Worker(url, options); + }; + } + + var WorkerFactory = createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () { + (function () { + '__worker_loader_strict__'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const lastBlobMap = new Map(); + const transparentBlobMap = new Map(); + function getTransparentBlobFor(width, height, dataURLOptions) { + return __awaiter(this, void 0, void 0, function* () { + const id = `${width}-${height}`; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) + return transparentBlobMap.get(id); + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + transparentBlobMap.set(id, base64); + return base64; + } + else { + return ''; + } + }); + } + const worker = self; + worker.onmessage = function (e) { + return __awaiter(this, void 0, void 0, function* () { + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor(width, height, dataURLOptions); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const type = blob.type; + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + if (!lastBlobMap.has(id) && (yield transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + if (lastBlobMap.get(id) === base64) + return worker.postMessage({ id }); + worker.postMessage({ + id, + type, + base64, + width, + height, + }); + lastBlobMap.set(id, base64); + } + else { + return worker.postMessage({ id: e.data.id }); + } + }); + }; + + })(); + }, null); + + class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = (target, mutation) => { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas, dataURLOptions, } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass, blockSelector); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { + dataURLOptions, + }); + } + initCanvasFPSObserver(fps, win, blockClass, blockSelector, options) { + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, true); + const snapshotInProgressMap = new Map(); + const worker = new WorkerFactory(); + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + if (!('base64' in e.data)) + return; + const { base64, type, width, height } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', + args: [0, 0, width, height], + }, + { + property: 'drawImage', + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + }, + 0, + 0, + ], + }, + ], + }); + }; + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId; + const getCanvas = () => { + const matchedCanvas = []; + win.document.querySelectorAll('canvas').forEach((canvas) => { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { + matchedCanvas.push(canvas); + } + }); + return matchedCanvas; + }; + const takeCanvasSnapshots = (timestamp) => { + if (lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + getCanvas() + .forEach((canvas) => __awaiter(this, void 0, void 0, function* () { + var _a; + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) + return; + if (canvas.width === 0 || canvas.height === 0) + return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes(canvas.__context)) { + const context = canvas.getContext(canvas.__context); + if (((_a = context === null || context === void 0 ? void 0 : context.getContextAttributes()) === null || _a === void 0 ? void 0 : _a.preserveDrawingBuffer) === false) { + context.clear(context.COLOR_BUFFER_BIT); + } + } + const bitmap = yield createImageBitmap(canvas); + worker.postMessage({ + id, + bitmap, + width: canvas.width, + height: canvas.height, + dataURLOptions: options.dataURLOptions, + }, [bitmap]); + })); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + rafId = requestAnimationFrame(takeCanvasSnapshots); + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + initCanvasMutationObserver(win, blockClass, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, false); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } + } + + class StylesheetManager { + constructor(options) { + this.trackedLinkElements = new WeakSet(); + this.styleMirror = new StyleSheetMirror(); + this.mutationCb = options.mutationCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; + } + attachLinkElement(linkEl, childSn) { + if ('_cssText' in childSn.attributes) + this.mutationCb({ + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: childSn.id, + attributes: childSn + .attributes, + }, + ], + }); + this.trackLinkElement(linkEl); + } + trackLinkElement(linkEl) { + if (this.trackedLinkElements.has(linkEl)) + return; + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + adoptStyleSheets(sheets, hostId) { + if (sheets.length === 0) + return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [], + }; + const styles = []; + for (const sheet of sheets) { + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + styles.push({ + styleId, + rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ + rule: stringifyRule(r), + index, + })), + }); + } + else + styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); + } + if (styles.length > 0) + adoptedStyleSheetData.styles = styles; + this.adoptedStyleSheetCb(adoptedStyleSheetData); + } + reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + trackStylesheetInLinkElement(linkEl) { + } + } + + class ProcessedNodeManager { + constructor() { + this.nodeMap = new WeakMap(); + this.loop = true; + this.periodicallyClear(); + } + periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + if (this.loop) + this.periodicallyClear(); + }); + } + inOtherBuffer(node, thisBuffer) { + const buffers = this.nodeMap.get(node); + return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + } + add(node, buffer) { + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + } + clear() { + this.nodeMap = new WeakMap(); + } + destroy() { + this.loop = false; + } + } + + function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: nowTimestamp() }); + } + let wrappedEmit; + let takeFullSnapshot; + let canvasManager; + let recording = false; + const mirror = createMirror(); + function record(options = {}) { + const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordDOM = true, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded' + ? options.recordAfter + : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; + registerErrorHandler(errorHandler); + const inEmittingFrame = recordCrossOriginIframes + ? window.parent === window + : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + if (window.parent.document) { + passEmitsToParent = false; + } + } + catch (e) { + passEmitsToParent = true; + } + } + if (inEmittingFrame && !emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + mirror.reset(); + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn && + !passEmitsToParent) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType$1.FullSnapshot && + !(e.type === EventType$1.IncrementalSnapshot && + e.data.source === IncrementalSource$1.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + if (inEmittingFrame) { + emit === null || emit === void 0 ? void 0 : emit(eventProcessor(e), isCheckout); + } + else if (passEmitsToParent) { + const message = { + type: 'rrweb', + event: eventProcessor(e), + origin: window.location.origin, + isCheckout, + }; + window.parent.postMessage(message, '*'); + } + if (e.type === EventType$1.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType$1.IncrementalSnapshot) { + if (e.data.source === IncrementalSource$1.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p), + })); + const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a), + })); + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, + }); + const iframeManager = new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + }); + } + const processedNodeManager = new ProcessedNodeManager(); + canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + if (!recordDOM) { + return; + } + wrappedEmit(wrapEvent({ + type: EventType$1.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + stylesheetManager.reset(); + shadowDomManager.init(); + mutationBuffers.forEach((buf) => buf.lock()); + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + dataURLOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.trackLinkElement(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachLinkElement(linkEl, childSn); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.FullSnapshot, + data: { + node, + initialOffset: getWindowScroll(window), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.unlock()); + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document)); + }; + try { + const handlers = []; + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Font }, p), + })), + selectionCb: (p) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Selection }, p), + })); + }, + customElementCb: (c) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CustomElement }, c), + })); + }, + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordDOM, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + dataURLOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + processedNodeManager, + canvasManager, + ignoreCSSAttributes, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType$1.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + recording = true; + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.DomContentLoaded, + data: {}, + })); + if (recordAfter === 'DOMContentLoaded') + init(); + })); + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.Load, + data: {}, + })); + if (recordAfter === 'load') + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + processedNodeManager.destroy(); + recording = false; + unregisterErrorHandler(); + }; + } + catch (error) { + console.warn(error); + } + } + record.addCustomEvent = (tag, payload) => { + if (!recording) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.Custom, + data: { + tag, + payload, + }, + })); + }; + record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); + }; + record.takeFullSnapshot = (isCheckout) => { + if (!recording) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); + }; + record.mirror = mirror; + + var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType || {}); + var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource || {}); + var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -66,7 +4573,7 @@ }; // Console override - var console = { + var console$1 = { /** @type {function(...*)} */ log: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { @@ -123,14 +4630,14 @@ var log_func_with_prefix = function(func, prefix) { return function() { arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); + return func.apply(console$1, arguments); }; }; var console_with_prefix = function(prefix) { return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) + log: log_func_with_prefix(console$1.log, prefix), + error: log_func_with_prefix(console$1.error, prefix), + critical: log_func_with_prefix(console$1.critical, prefix) }; }; @@ -983,7 +5490,7 @@ try { result = decodeURIComponent(result); } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); + console$1.error('Skipping decoding for malformed query param: ' + result); } return result.replace(/\+/g, ' '); } @@ -1110,13 +5617,13 @@ is_supported: function(force_check) { var supported = localStorageSupported(null, force_check); if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); + console$1.error('localStorage unsupported; falling back to cookie store'); } return supported; }, error: function(msg) { - console.error('localStorage error: ' + msg); + console$1.error('localStorage error: ' + msg); }, get: function(name) { @@ -1171,7 +5678,7 @@ */ var register_event = function(element, type, handler, oldSchool, useCapture) { if (!element) { - console.error('No valid element provided to register_event'); + console$1.error('No valid element provided to register_event'); return; } @@ -1733,158 +6240,306 @@ _['info']['browserVersion'] = _.info.browserVersion; _['info']['properties'] = _.info.properties; - /* eslint camelcase: "off" */ - /** - * DomTracker Object - * @constructor + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. */ - var DomTracker = function() {}; - - - // interface - DomTracker.prototype.create_properties = function() {}; - DomTracker.prototype.event_handler = function() {}; - DomTracker.prototype.after_track_handler = function() {}; - - DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; - }; /** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ - DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - that.event_handler(e, this, options); + /** Public **/ - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); + /** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function optIn(token, options) { + _optInOut(true, token, options); + } - return true; - }; + /** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ + function optOut(token, options) { + _optInOut(false, token, options); + } /** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type */ - DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; + function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; + } - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; + /** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ + function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console$1.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console$1.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; + } - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } + /** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); + } - that.after_track_handler(props, options, timeout_occured); - }; - }; + /** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); + } - DomTracker.prototype.create_properties = function(properties, element) { - var props; + /** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); + } - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } + /** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); + } - return props; - }; + /** Private **/ /** - * LinkTracker Object - * @constructor - * @extends DomTracker + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage */ - var LinkTracker = function() { - this.override_event = 'click'; - }; - _.inherit(LinkTracker, DomTracker); + function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; + } - LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); + /** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ + function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; + } - if (element.href) { props['url'] = element.href; } + /** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ + function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); + } - return props; - }; + /** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ + function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; - LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); - if (!options.new_tab) { - evt.preventDefault(); + return hasDntOn; + } + + /** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console$1.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; } - }; - LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } + options = options || {}; - setTimeout(function() { - window.location = options.href; - }, 0); - }; + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } + } /** - * FormTracker Object - * @constructor - * @extends DomTracker + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out */ - var FormTracker = function() { - this.override_event = 'submit'; - }; - _.inherit(FormTracker, DomTracker); + function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; - FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); - }; + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests - FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); - }; + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console$1.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } - var logger$2 = console_with_prefix('lock'); + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; + } + + var logger$3 = console_with_prefix('lock'); /** * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser @@ -1941,7 +6596,7 @@ var delay = function(cb) { if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); storage.removeItem(keyZ); storage.removeItem(keyY); loop(); @@ -2030,7 +6685,7 @@ } }; - var logger$1 = console_with_prefix('batch'); + var logger$2 = console_with_prefix('batch'); /** * RequestQueue: queue for batching API requests with localStorage backup for retries. @@ -2052,9 +6707,10 @@ options = options || {}; this.storageKey = storageKey; this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2079,29 +6735,36 @@ 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2112,7 +6775,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2161,61 +6824,67 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2243,25 +6912,32 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } + if (!this.usePersistence) { if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2304,13 +6980,16 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - var logger = console_with_prefix('batch'); + var logger$1 = console_with_prefix('batch'); /** * RequestBatcher: manages the queueing, flushing, retry etc of requests of one @@ -2322,7 +7001,8 @@ this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2339,6 +7019,11 @@ // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2414,7 +7099,7 @@ try { if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); + logger$1.log('Flush: Request already in progress'); return; } @@ -2423,6 +7108,9 @@ var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2490,22 +7178,17 @@ this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2529,7 +7212,11 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2575,9 +7262,8 @@ if (options.unloading) { requestOptions.transport = 'sendBeacon'; } - logger.log('MIXPANEL REQUEST:', dataForRequest); + logger$1.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2588,7 +7274,7 @@ * Log error to global logger and optional user-defined logger. */ RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); + logger$1.error.apply(logger$1.error, arguments); if (this.errorReporter) { try { if (!(err instanceof Error)) { @@ -2596,309 +7282,402 @@ } this.errorReporter(msg, err); } catch(err) { - logger.error(err); + logger$1.error(err); } } }; - /** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - - /** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ + var logger = console_with_prefix('recorder'); + var CompressionStream = win['CompressionStream']; - /** Public **/ + var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true + }; - var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, + ]); - /** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ - function optIn(token, options) { - _optInOut(true, token, options); + function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); } - /** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ - function optOut(token, options) { - _optInOut(false, token, options); - } + var MixpanelRecorder = function(mixpanelInstance) { + this._mixpanel = mixpanelInstance; - /** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ - function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; - } + // internal rrweb stopRecording function + this._stopRecording = null; - /** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ - function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; + this.recEvents = []; + this.seqNo = 0; + this.replayId = null; + this.replayStartTime = null; + this.sendBatchId = null; + + this.idleTimeoutId = null; + this.maxTimeoutId = null; + + this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); + }; + + + MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); + }; + + // eslint-disable-next-line camelcase + MixpanelRecorder.prototype.get_config = function(configVar) { + return this._mixpanel.get_config(configVar); + }; + + MixpanelRecorder.prototype.startRecording = function () { + if (this._stopRecording !== null) { + logger.log('Recording already in progress, skipping startRecording.'); + return; } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + + this.recordMaxMs = this.get_config('record_max_ms'); + if (this.recordMaxMs > MAX_RECORDING_MS) { + this.recordMaxMs = MAX_RECORDING_MS; + logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.'); } - return optedOut; - } - /** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ - function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); - } + this.recEvents = []; + this.seqNo = 0; + this.replayStartTime = null; - /** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ - function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + this.replayId = _.UUID(); + + this.batcher.start(); + + var resetIdleTimeout = _.bind(function () { + clearTimeout(this.idleTimeoutId); + this.idleTimeoutId = setTimeout(_.bind(function () { + logger.log('Idle timeout reached, restarting recording.'); + this.resetRecording(); + }, this), this.get_config('record_idle_timeout_ms')); + }, this); + + this._stopRecording = record({ + 'emit': _.bind(function (ev) { + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } + }, this), + 'blockClass': this.get_config('record_block_class'), + 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, + 'maskTextClass': this.get_config('record_mask_text_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); - } + + resetIdleTimeout(); + + this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); + }; + + MixpanelRecorder.prototype.resetRecording = function () { + this.stopRecording(); + this.startRecording(); + }; + + MixpanelRecorder.prototype.stopRecording = function () { + if (this._stopRecording !== null) { + this._stopRecording(); + this._stopRecording = null; + } + + this.batcher.flush(); // flush any remaining events + this.replayId = null; + + clearTimeout(this.idleTimeoutId); + clearTimeout(this.maxTimeoutId); + }; /** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out + * Flushes the current batch of events to the server, but passes an opt-out callback to make sure + * we stop recording and dump any queued events if the user has opted out. */ - function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); + }; + + MixpanelRecorder.prototype._onOptOut = function (code) { + // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out + if (code === 0) { + this.recEvents = []; + this.stopRecording(); + } + }; + + MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/octet-stream' + }, + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); - } + }; - /** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ - function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); - } + MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + + var reqParams = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + var eventsJson = _.JSONEncode(data); + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqParams['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqParams['$user_id'] = userId; + } + + if (CompressionStream) { + var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); + var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); + new Response(gzipStream) + .blob() + .then(_.bind(function(compressedBlob) { + reqParams['format'] = 'gzip'; + this._sendRequest(reqParams, compressedBlob, callback); + }, this)); + } else { + reqParams['format'] = 'body'; + this._sendRequest(reqParams, eventsJson, callback); + } + } + }); + + + MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } + }; - /** Private **/ - /** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ - function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; - } + win['__mp_recorder'] = MixpanelRecorder; - /** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ - function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; - } + /* eslint camelcase: "off" */ /** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type + * DomTracker Object + * @constructor */ - function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); - } + var DomTracker = function() {}; - /** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ - function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); + // interface + DomTracker.prototype.create_properties = function() {}; + DomTracker.prototype.event_handler = function() {}; + DomTracker.prototype.after_track_handler = function() {}; - return hasDntOn; - } + DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; + }; /** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback */ - function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); return; } - options = options || {}; + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); + that.event_handler(e, this, options); - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); }); - } - } + }, this); + + return true; + }; /** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured */ - function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; + DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; } - if (!optedOut) { - return method.apply(this, arguments); - } + that.after_track_handler(props, options, timeout_occured); + }; + }; - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } + DomTracker.prototype.create_properties = function(properties, element) { + var props; - return; - }; - } + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; + }; + + /** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ + var LinkTracker = function() { + this.override_event = 'click'; + }; + _.inherit(LinkTracker, DomTracker); + + LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; + }; + + LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } + }; + + LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); + }; + + /** + * FormTracker Object + * @constructor + * @extends DomTracker + */ + var FormTracker = function() { + this.override_event = 'submit'; + }; + _.inherit(FormTracker, DomTracker); + + FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); + }; + + FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); + }; /* eslint camelcase: "off" */ @@ -3320,7 +8099,7 @@ _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); return; } else { $add[k] = v; @@ -3445,7 +8224,7 @@ if (!_.isNumber(amount)) { amount = parseFloat(amount); if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } @@ -3482,7 +8261,7 @@ */ MixpanelPeople.prototype.delete_user = function() { if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); return; } var data = {'$delete': this._mixpanel.get_distinct_id()}; @@ -3556,7 +8335,7 @@ } else if (UNION_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); } else { - console.error('Invalid call to _enqueue():', data); + console$1.error('Invalid call to _enqueue():', data); } }; @@ -3704,7 +8483,7 @@ var storage_type = config['persistence']; if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); storage_type = config['persistence'] = 'cookie'; } @@ -4008,8 +8787,8 @@ this._pop_from_people_queue(UNSET_ACTION, q_data); } - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); this.save(); }; @@ -4054,7 +8833,7 @@ } else if (queue === UNION_ACTION) { return UNION_QUEUE_KEY; } else { - console.error('Invalid queue:', queue); + console$1.error('Invalid queue:', queue); } }; @@ -4111,6 +8890,12 @@ */ var init_type; // MODULE or SNIPPET loader + // allow bundlers to specify how extra code (recorder bundle) should be loaded + // eslint-disable-next-line no-unused-vars + var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); + }; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -4204,7 +8989,9 @@ 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4237,7 +9024,7 @@ instance = target; } else { if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); + console$1.error('You have already initialized ' + name); return; } instance = new MixpanelLib(); @@ -4365,9 +9152,9 @@ if (this._batch_requests) { if (!_.localStorage.is_supported(true) || !USE_XHR) { this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); + console$1.log('Clearing batch queue ' + batcher_config.queue_key); _.localStorage.remove(batcher_config.queue_key); }); } else { @@ -4430,7 +9217,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); + console$1.critical('Browser does not support MutationObserver; skipping session recording'); return; } @@ -4440,12 +9227,7 @@ }, this); if (_.isUndefined(win['__mp_recorder'])) { - var scriptEl = document$1.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document$1.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -4455,7 +9237,7 @@ if (this._recorder) { this._recorder['stopRecording'](); } else { - console.critical('Session recorder module not loaded'); + console$1.critical('Session recorder module not loaded'); } }; @@ -4744,7 +9526,8 @@ lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4844,6 +9627,7 @@ attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4855,8 +9639,8 @@ beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); @@ -4948,8 +9732,8 @@ truncated_data = this._run_hook('before_send_' + options.type, truncated_data); } if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); return this._send_request( endpoint, this._encode_data_for_request(truncated_data), @@ -6146,14 +10930,14 @@ }; MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); + console$1.error.apply(console$1.error, arguments); try { if (!err && !(msg instanceof Error)) { msg = new Error(msg); } this.get_config('error_reporter')(msg, err); } catch(err) { - console.error(err); + console$1.error(err); } }; @@ -6305,7 +11089,8 @@ _.register_event(win, 'load', dom_loaded_handler, true); }; - function init_as_module() { + function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); @@ -6316,9 +11101,16 @@ return mixpanel_master; } + // For loading separate bundles asynchronously via script tag + + // For builds that have everything in one bundle, no extra work. + function loadNoop (_src, onload) { + onload(); + } + /* eslint camelcase: "off" */ - var mixpanel = init_as_module(); + var mixpanel = init_as_module(loadNoop); return mixpanel; diff --git a/examples/commonjs-browserify/bundle.js b/examples/commonjs-browserify/bundle.js index 1ce5fc3f..2a964257 100644 --- a/examples/commonjs-browserify/bundle.js +++ b/examples/commonjs-browserify/bundle.js @@ -1,9 +1,4516 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} +function createMirror() { + return new Mirror(); +} +function maskInputValue({ element, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + const actualType = type && toLowerCase(type); + if (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType])) { + if (maskInputFn) { + text = maskInputFn(text, element); + } + else { + text = '*'.repeat(text.length); + } + } + return text; +} +function toLowerCase(str) { + return str.toLowerCase(); +} +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; +} +function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? + toLowerCase(type) + : null; +} +function extractFileExtension(path, baseURL) { + var _a; + let url; + try { + url = new URL(path, baseURL !== null && baseURL !== void 0 ? baseURL : window.location.href); + } + catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; + const match = url.pathname.match(regex); + return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null; +} + +let _id = 1; +const tagNameRegex = new RegExp('[^a-z0-9-_:]'); +const IGNORED_NODE = -2; +function genId() { + return _id++; +} +function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; +} +function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; +} +let canvasService; +let canvasCtx; +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; +const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; +const URL_WWW_MATCH = /^www\..*/i; +const DATA_URI = /^(data:)([^,]*),(.*)/i; +function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); +} +const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; +const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; +function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + const output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); +} +function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; +} +function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); +} +function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; +} +function transformAttribute(doc, tagName, name, value) { + if (!value) { + return value; + } + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + return value; +} +function ignoreAttribute(tagName, name, _value) { + return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; +} +function _isBlockedElement(element, blockClass, blockSelector) { + try { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = element.classList.length; eIndex--;) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + } + catch (e) { + } + return false; +} +function classMatchesRegex(node, regex, checkAncestors) { + if (!node) + return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + for (let eIndex = node.classList.length; eIndex--;) { + const className = node.classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); +} +function needMaskingText(node, maskTextClass, maskTextSelector, checkAncestors) { + try { + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + if (el === null) + return false; + if (typeof maskTextClass === 'string') { + if (checkAncestors) { + if (el.closest(`.${maskTextClass}`)) + return true; + } + else { + if (el.classList.contains(maskTextClass)) + return true; + } + } + else { + if (classMatchesRegex(el, maskTextClass, checkAncestors)) + return true; + } + if (maskTextSelector) { + if (checkAncestors) { + if (el.closest(maskTextSelector)) + return true; + } + else { + if (el.matches(maskTextSelector)) + return true; + } + } + } + catch (e) { + } + return false; +} +function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); + } + iframeEl.addEventListener('load', listener); +} +function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; + } + catch (error) { + return; + } + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); +} +function serializeNode(n, options) { + const { doc, mirror, blockClass, blockSelector, needsMask, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options; + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: n.compatMode, + }; + } + else { + return { + type: NodeType.Document, + childNodes: [], + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n, { + needsMask, + maskTextFn, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } +} +function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; +} +function serializeTextNode(n, options) { + var _a; + const { needsMask, maskTextFn, rootId } = options; + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStylesheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (!isStyle && !isScript && textContent && needsMask) { + textContent = maskTextFn + ? maskTextFn(textContent, n.parentElement) + : textContent.replace(/[\S]/g, '*'); + } + return { + type: NodeType.Text, + textContent: textContent || '', + isStyle, + rootId, + }; +} +function serializeElementNode(n, options) { + const { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions = {}, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, } = options; + const needBlock = _isBlockedElement(n, blockClass, blockSelector); + const tagName = getValidTagName(n); + let attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = stringifyStylesheet(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = n.value; + const checked = n.checked; + if (attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value) { + attributes.value = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } + else if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + image.removeEventListener('load', recordInlineImage); + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.addEventListener('load', recordInlineImage); + } + if (tagName === 'audio' || tagName === 'video') { + const mediaAttributes = attributes; + mediaAttributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + mediaAttributes.rr_mediaCurrentTime = n.currentTime; + mediaAttributes.rr_mediaPlaybackRate = n.playbackRate; + mediaAttributes.rr_mediaMuted = n.muted; + mediaAttributes.rr_mediaLoop = n.loop; + mediaAttributes.rr_mediaVolume = n.volume; + } + if (!newlyAddedElement) { + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + let isCustomElement; + try { + if (customElements.get(tagName)) + isCustomElement = true; + } + catch (e) { + } + return { + type: NodeType.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + isCustom: isCustomElement, + }; +} +function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } +} +function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + return true; + } + else if (sn.type === NodeType.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + extractFileExtension(sn.attributes.href) === 'js'))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; +} +function serializeNodeWithId(n, options) { + const { doc, mirror, blockClass, blockSelector, maskTextClass, maskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options; + let { needsMask } = options; + let { preserveWhiteSpace = true } = options; + if (!needsMask && + n.childNodes) { + const checkAncestors = needsMask === undefined; + needsMask = needMaskingText(n, maskTextClass, maskTextSelector, checkAncestors); + } + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if (mirror.hasNode(n)) { + id = mirror.getId(n); + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + mirror.add(n, serializedNode); + if (id === IGNORED_NODE) { + return null; + } + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + const shadowRoot = n.shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType.Document || + serializedNode.type === NodeType.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }; + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'textarea' && + serializedNode.attributes.value !== undefined) ; + else { + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css'))) { + onceStylesheetLoaded(n, () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedLinkNode) { + onStylesheetLoad(n, serializedLinkNode); + } + } + }, stylesheetLoadTimeout); + } + return serializedNode; +} +function snapshot(n, options) { + const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + }); +} + +function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); +} +const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; +let _mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, +}; +if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); +} +function throttle(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (...args) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; +} +function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); +} +function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { + }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { + }; + } +} +let nowTimestamp = Date.now; +if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) { + nowTimestamp = () => new Date().getTime(); +} +function getWindowScroll(win) { + var _a, _b, _c, _d, _e, _f; + const doc = win.document; + return { + left: doc.scrollingElement + ? doc.scrollingElement.scrollLeft + : win.pageXOffset !== undefined + ? win.pageXOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollLeft) || + ((_b = (_a = doc === null || doc === void 0 ? void 0 : doc.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || + ((_c = doc === null || doc === void 0 ? void 0 : doc.body) === null || _c === void 0 ? void 0 : _c.scrollLeft) || + 0, + top: doc.scrollingElement + ? doc.scrollingElement.scrollTop + : win.pageYOffset !== undefined + ? win.pageYOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollTop) || + ((_e = (_d = doc === null || doc === void 0 ? void 0 : doc.body) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.scrollTop) || + ((_f = doc === null || doc === void 0 ? void 0 : doc.body) === null || _f === void 0 ? void 0 : _f.scrollTop) || + 0, + }; +} +function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); +} +function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); +} +function closestElementOfNode(node) { + if (!node) { + return null; + } + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + return el; +} +function isBlocked(node, blockClass, blockSelector, checkAncestors) { + if (!node) { + return false; + } + const el = closestElementOfNode(node); + if (!el) { + return false; + } + try { + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) + return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) + return true; + } + else { + if (classMatchesRegex(el, blockClass, checkAncestors)) + return true; + } + } + catch (e) { + } + if (blockSelector) { + if (el.matches(blockSelector)) + return true; + if (checkAncestors && el.closest(blockSelector) !== null) + return true; + } + return false; +} +function isSerialized(n, mirror) { + return mirror.getId(n) !== -1; +} +function isIgnored(n, mirror) { + return mirror.getId(n) === IGNORED_NODE; +} +function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); +} +function legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); +} +function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = (...args) => { + let node = args[0]; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } +} +function isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); +} +function isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); +} +function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); +} +class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + } + getId(stylesheet) { + var _a; + return (_a = this.styleIDMap.get(stylesheet)) !== null && _a !== void 0 ? _a : -1; + } + has(stylesheet) { + return this.styleIDMap.has(stylesheet); + } + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; + } + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + getStyle(id) { + return this.idStyleMap.get(id) || null; + } + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; + } + generateId() { + return this.id++; + } +} +function getShadowHost(n) { + var _a, _b; + let shadowHost = null; + if (((_b = (_a = n.getRootNode) === null || _a === void 0 ? void 0 : _a.call(n)) === null || _b === void 0 ? void 0 : _b.nodeType) === Node.DOCUMENT_FRAGMENT_NODE && + n.getRootNode().host) + shadowHost = n.getRootNode().host; + return shadowHost; +} +function getRootShadowHost(n) { + let rootShadowHost = n; + let shadowHost; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; + return rootShadowHost; +} +function shadowHostInDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + const shadowHost = getRootShadowHost(n); + return doc.contains(shadowHost); +} +function inDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + return doc.contains(n) || shadowHostInDom(n); +} + +var EventType$1 = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; +})(EventType$1 || {}); +var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; +})(IncrementalSource$1 || {}); +var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; +})(MouseInteractions || {}); +var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; +})(PointerTypes || {}); +var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; +})(CanvasContext || {}); + +function isNodeInLinkedList(n) { + return '__ln' in n; +} +class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + this.tail = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + if (node.next === null) { + this.tail = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + else { + this.tail = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } +} +const moveKey = (id, parentId) => `${id}@${parentId}`; +class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addedIds = new Set(); + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + if (!n.parentNode || + !inDom(n) || + n.parentNode.tagName === 'TEXTAREA') { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + let tailNode = addList.tail; + while (tailNode) { + const _node = tailNode; + tailNode = tailNode.previous; + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (nextId === -1) + continue; + else if (parentId !== -1) { + node = _node; + break; + } + else { + const unhandledNode = _node.value; + if (unhandledNode.parentNode && + unhandledNode.parentNode.nodeType === + Node.DOCUMENT_FRAGMENT_NODE) { + const shadowHost = unhandledNode.parentNode + .host; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => { + const n = text.node; + if (n.parentNode && + n.parentNode.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(n.parentNode); + } + return { + id: this.mirror.getId(n), + value: text.value, + }; + }) + .filter((text) => !addedIds.has(text.id)) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => { + const { attributes } = attribute; + if (typeof attributes.style === 'string') { + const diffAsStr = JSON.stringify(attribute.styleDiff); + const unchangedAsStr = JSON.stringify(attribute._unchangedStyles); + if (diffAsStr.length < attributes.style.length) { + if ((diffAsStr + unchangedAsStr).split('var(').length === + attributes.style.split('var(').length) { + attributes.style = attribute.styleDiff; + } + } + } + return { + id: this.mirror.getId(attribute.node), + attributes: attributes, + }; + }) + .filter((attribute) => !addedIds.has(attribute.id)) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.genTextAreaValueMutation = (textarea) => { + let item = this.attributeMap.get(textarea); + if (!item) { + item = { + node: textarea, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(textarea, item); + } + item.attributes.value = Array.from(textarea.childNodes, (cn) => cn.textContent || '').join(''); + }; + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, false) && + value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, true) && value + ? this.maskTextFn + ? this.maskTextFn(value, closestElementOfNode(m.target)) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let attributeName = m.attributeName; + let value = m.target.getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ + element: target, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type, + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + value === m.oldValue) { + return; + } + let item = this.attributeMap.get(m.target); + if (target.tagName === 'IFRAME' && + attributeName === 'src' && + !this.keepIframeSrcFn(value)) { + if (!target.contentDocument) { + attributeName = 'rr_src'; + } + else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(m.target, item); + } + if (attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (!ignoreAttribute(target.tagName, attributeName)) { + item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value); + if (attributeName === 'style') { + if (!this.unattachedDoc) { + try { + this.unattachedDoc = + document.implementation.createHTMLDocument(); + } + catch (e) { + this.unattachedDoc = this.doc; + } + } + const old = this.unattachedDoc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + item.styleDiff[pname] = newValue; + } + else { + item.styleDiff[pname] = [newValue, newPriority]; + } + } + else { + item._unchangedStyles[pname] = [newValue, newPriority]; + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + item.styleDiff[pname] = false; + } + } + } + } + break; + } + case 'childList': { + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; + if (m.target.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(m.target); + return; + } + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (this.processedNodeManager.inOtherBuffer(n, this)) + return; + if (this.addedSet.has(n) || this.movedSet.has(n)) + return; + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); + } + } + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'dataURLOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'processedNodeManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } +} +function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); +} +function isParentRemoved(removes, n, mirror) { + if (removes.length === 0) + return false; + return _isParentRemoved(removes, n, mirror); +} +function _isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); +} +function isAncestorInSet(set, n) { + if (set.size === 0) + return false; + return _isAncestorInSet(set, n); +} +function _isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); +} + +let errorHandler; +function registerErrorHandler(handler) { + errorHandler = handler; +} +function unregisterErrorHandler() { + errorHandler = undefined; +} +const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { + try { + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return; + } + throw error; + } + }); + return rrwebWrapped; +}; + +const mutationBuffers = []; +function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { + } + return event && event.target; +} +function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer))); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; +} +function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { + }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle(callbackWrapper((source) => { + const totalOffset = Date.now() - timeBaseline; + mousemoveCb(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }), callbackThreshold); + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = legacy_isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = nowTimestamp(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: nowTimestamp() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource$1.Drag + : evt instanceof MouseEvent + ? IncrementalSource$1.MouseMove + : IncrementalSource$1.TouchMove); + }), threshold, { + trailing: false, + })); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { + }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + let currentPointerType = null; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, true)) { + return; + } + let pointerType = null; + let thisEventKey = eventKey; + if ('pointerType' in event) { + switch (event.pointerType) { + case 'mouse': + pointerType = PointerTypes.Mouse; + break; + case 'touch': + pointerType = PointerTypes.Touch; + break; + case 'pen': + pointerType = PointerTypes.Pen; + break; + } + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + thisEventKey = 'TouchStart'; + } + else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) { + thisEventKey = 'TouchEnd'; + } + } + else if (pointerType === PointerTypes.Pen) ; + } + else if (legacy_isTouchEvent(event)) { + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + if ((thisEventKey.startsWith('Touch') && + pointerType === PointerTypes.Touch) || + (thisEventKey.startsWith('Mouse') && + pointerType === PointerTypes.Mouse)) { + pointerType = null; + } + } + else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; + } + const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)(Object.assign({ type: MouseInteractions[thisEventKey], id, x: clientX, y: clientY }, (pointerType !== null && { pointerType }))); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + let eventName = toLowerCase(eventKey); + const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + return; + } + } + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }) { + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); + } + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }), sampling.scroll || 100)); + return on('scroll', updatePosition, doc); +} +function initViewportResizeObserver({ viewportResizeCb }, { win }) { + let lastH = -1; + let lastW = -1; + const updateDimension = callbackWrapper(throttle(callbackWrapper(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }), 200)); + return on('resize', updateDimension, win); +} +const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; +const lastInputValueMap = new WeakMap(); +function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + if (target.classList.contains(ignoreClass) || + (ignoreSelector && target.matches(ignoreSelector))) { + return; + } + let text = target.value; + let isChecked = false; + const type = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + else if (maskInputOptions[tagName.toLowerCase()] || + maskInputOptions[type]) { + text = maskInputValue({ + element: target, + maskInputOptions, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, userTriggeredOnInput + ? { text, isChecked, userTriggered } + : { text, isChecked }); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = el.value; + cbWithDedup(el, userTriggeredOnInput + ? { text, isChecked: !isChecked, userTriggered: false } + : { text, isChecked: !isChecked }); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + callbackWrapper(inputCb)(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const currentWindow = doc.defaultView; + if (!currentWindow) { + return () => { + handlers.forEach((h) => h()); + }; + } + const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [currentWindow.HTMLInputElement.prototype, 'value'], + [currentWindow.HTMLInputElement.prototype, 'checked'], + [currentWindow.HTMLSelectElement.prototype, 'value'], + [currentWindow.HTMLTextAreaElement.prototype, 'value'], + [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'], + [currentWindow.HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ + target: this, + isTrusted: false, + }); + }, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); +} +function getIdAndStyleId(sheet, mirror, styleMirror) { + let id, styleId; + if (!sheet) + return {}; + if (sheet.ownerNode) + id = mirror.getId(sheet.ownerNode); + else + styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; +} +function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { + }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + let replace; + if (win.CSSStyleSheet.prototype.replace) { + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = new Proxy(replace, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + let replaceSync; + if (win.CSSStyleSheet.prototype.replaceSync) { + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); +} +function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) { + var _a, _b, _c; + let hostId = null; + if (host.nodeName === '#document') + hostId = mirror.getId(host); + else + hostId = mirror.getId(host.host); + const patchTarget = host.nodeName === '#document' + ? (_a = host.defaultView) === null || _a === void 0 ? void 0 : _a.Document + : (_c = (_b = host.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.ShadowRoot; + const originalPropertyDescriptor = (patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype) + ? Object.getOwnPropertyDescriptor(patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype, 'adoptedStyleSheets') + : undefined; + if (hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor) + return () => { + }; + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get() { + var _a; + return (_a = originalPropertyDescriptor.get) === null || _a === void 0 ? void 0 : _a.call(this); + }, + set(sheets) { + var _a; + const result = (_a = originalPropertyDescriptor.set) === null || _a === void 0 ? void 0 : _a.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } + catch (e) { + } + } + return result; + }, + }); + return callbackWrapper(() => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get: originalPropertyDescriptor.get, + set: originalPropertyDescriptor.set, + }); + }); +} +function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property, value, priority] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return setProperty.apply(thisArg, [property, value, priority]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return removeProperty.apply(thisArg, [property]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); +} +function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, mirror, sampling, doc, }) { + const handler = callbackWrapper((type) => throttle(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const { currentTime, volume, muted, playbackRate, loop } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + playbackRate, + loop, + }); + }), sampling.media || 500)); + const handlers = [ + on('play', handler(0), doc), + on('pause', handler(1), doc), + on('seeked', handler(2), doc), + on('volumechange', handler(3), doc), + on('ratechange', handler(4), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { + }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(callbackWrapper(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }), 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initSelectionObserver(param) { + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; + let collapsed = true; + const updateSelection = callbackWrapper(() => { + const selection = doc.getSelection(); + if (!selection || (collapsed && (selection === null || selection === void 0 ? void 0 : selection.isCollapsed))) + return; + collapsed = selection.isCollapsed || false; + const ranges = []; + const count = selection.rangeCount || 0; + for (let i = 0; i < count; i++) { + const range = selection.getRangeAt(i); + const { startContainer, startOffset, endContainer, endOffset } = range; + const blocked = isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); + if (blocked) + continue; + ranges.push({ + start: mirror.getId(startContainer), + startOffset, + end: mirror.getId(endContainer), + endOffset, + }); + } + selectionCb({ ranges }); + }); + updateSelection(); + return on('selectionchange', updateSelection); +} +function initCustomElementObserver({ doc, customElementCb, }) { + const win = doc.defaultView; + if (!win || !win.customElements) + return () => { }; + const restoreHandler = patch(win.customElements, 'define', function (original) { + return function (name, constructor, options) { + try { + customElementCb({ + define: { + name, + }, + }); + } + catch (e) { + console.warn(`Custom element callback failed for ${name}`); + } + return original.apply(this, [name, constructor, options]); + }; + }); + return restoreHandler; +} +function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, selectionCb, customElementCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; + o.selectionCb = (...p) => { + if (hooks.selection) { + hooks.selection(...p); + } + selectionCb(...p); + }; + o.customElementCb = (...c) => { + if (hooks.customElement) { + hooks.customElement(...c); + } + customElementCb(...c); + }; +} +function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { + }; + } + mergeHooks(o, hooks); + let mutationObserver; + if (o.recordDOM) { + mutationObserver = initMutationObserver(o, o.doc); + } + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o, { + win: currentWindow, + }); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + let styleSheetObserver = () => { }; + let adoptedStyleSheetObserver = () => { }; + let styleDeclarationObserver = () => { }; + let fontObserver = () => { }; + if (o.recordDOM) { + styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); + styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + if (o.collectFonts) { + fontObserver = initFontObserver(o); + } + } + const selectionObserver = initSelectionObserver(o); + const customElementObserver = initCustomElementObserver(o); + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver === null || mutationObserver === void 0 ? void 0 : mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + adoptedStyleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + selectionObserver(); + customElementObserver(); + pluginHandlers.forEach((h) => h()); + }); +} +function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; +} +function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); +} + +class CrossOriginIframeMirror { + constructor(generateIdFn) { + this.generateIdFn = generateIdFn; + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + } + getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) { + const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe); + let id = idToRemoteIdMap.get(remoteId); + if (!id) { + id = this.generateIdFn(); + idToRemoteIdMap.set(remoteId, id); + remoteIdToIdMap.set(id, remoteId); + } + return id; + } + getIds(iframe, remoteId) { + const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + } + getRemoteId(iframe, id, map) { + const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe); + if (typeof id !== 'number') + return id; + const remoteId = remoteIdToIdMap.get(id); + if (!remoteId) + return -1; + return remoteId; + } + getRemoteIds(iframe, ids) { + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + } + reset(iframe) { + if (!iframe) { + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + return; + } + this.iframeIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToIdMap.delete(iframe); + } + getIdToRemoteIdMap(iframe) { + let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); + if (!idToRemoteIdMap) { + idToRemoteIdMap = new Map(); + this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + } + return idToRemoteIdMap; + } + getRemoteIdToIdMap(iframe) { + let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); + if (!remoteIdToIdMap) { + remoteIdToIdMap = new Map(); + this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + } + return remoteIdToIdMap; + } +} + +class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.crossOriginIframeMap = new WeakMap(); + this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + this.crossOriginIframeRootIdMap = new WeakMap(); + this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; + this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)); + this.mirror = options.mirror; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + if (iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0) + this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument)); + } + handleMessage(message) { + const crossOriginMessageEvent = message; + if (crossOriginMessageEvent.data.type !== 'rrweb' || + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin) + return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) + return; + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) + return; + const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event); + if (transformedEvent) + this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout); + } + transformCrossOriginEvent(iframeEl, e) { + var _a; + switch (e.type) { + case EventType$1.FullSnapshot: { + this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); + this.replaceIdOnNode(e.data.node, iframeEl); + const rootId = e.data.node.id; + this.crossOriginIframeRootIdMap.set(iframeEl, rootId); + this.patchRootIdOnNode(e.data.node, rootId); + return { + timestamp: e.timestamp, + type: EventType$1.IncrementalSnapshot, + data: { + source: IncrementalSource$1.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + case EventType$1.Meta: + case EventType$1.Load: + case EventType$1.DomContentLoaded: { + return false; + } + case EventType$1.Plugin: { + return e; + } + case EventType$1.Custom: { + this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); + return e; + } + case EventType$1.IncrementalSnapshot: { + switch (e.data.source) { + case IncrementalSource$1.Mutation: { + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, [ + 'parentId', + 'nextId', + 'previousId', + ]); + this.replaceIdOnNode(n.node, iframeEl); + const rootId = this.crossOriginIframeRootIdMap.get(iframeEl); + rootId && this.patchRootIdOnNode(n.node, rootId); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.Drag: + case IncrementalSource$1.TouchMove: + case IncrementalSource$1.MouseMove: { + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.ViewportResize: { + return false; + } + case IncrementalSource$1.MediaInteraction: + case IncrementalSource$1.MouseInteraction: + case IncrementalSource$1.Scroll: + case IncrementalSource$1.CanvasMutation: + case IncrementalSource$1.Input: { + this.replaceIds(e.data, iframeEl, ['id']); + return e; + } + case IncrementalSource$1.StyleSheetRule: + case IncrementalSource$1.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleId']); + return e; + } + case IncrementalSource$1.Font: { + return e; + } + case IncrementalSource$1.Selection: { + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); + return e; + } + case IncrementalSource$1.AdoptedStyleSheet: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); + return e; + } + } + } + } + return false; + } + replace(iframeMirror, obj, iframeEl, keys) { + for (const key of keys) { + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') + continue; + if (Array.isArray(obj[key])) { + obj[key] = iframeMirror.getIds(iframeEl, obj[key]); + } + else { + obj[key] = iframeMirror.getId(iframeEl, obj[key]); + } + } + return obj; + } + replaceIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); + } + replaceStyleIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); + } + replaceIdOnNode(node, iframeEl) { + this.replaceIds(node, iframeEl, ['id', 'rootId']); + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } + patchRootIdOnNode(node, rootId) { + if (node.type !== NodeType.Document && !node.rootId) + node.rootId = rootId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.patchRootIdOnNode(child, rootId); + }); + } + } +} + +class ShadowDomManager { + constructor(options) { + this.shadowDoms = new WeakSet(); + this.restoreHandlers = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + this.init(); + } + init() { + this.reset(); + this.patchAttachShadow(Element, document); + } + addShadowRoot(shadowRoot, doc) { + if (!isNativeShadowDom(shadowRoot)) + return; + if (this.shadowDoms.has(shadowRoot)) + return; + this.shadowDoms.add(shadowRoot); + const observer = initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + this.restoreHandlers.push(() => observer.disconnect()); + this.restoreHandlers.push(initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror }))); + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0) + this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host)); + this.restoreHandlers.push(initAdoptedStyleSheetObserver({ + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, shadowRoot)); + }, 0); + } + observeAttachShadow(iframeElement) { + if (!iframeElement.contentWindow || !iframeElement.contentDocument) + return; + this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument); + } + patchAttachShadow(element, doc) { + const manager = this; + this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) { + return function (option) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot && inDom(this)) + manager.addShadowRoot(this.shadowRoot, doc); + return shadowRoot; + }; + })); + } + reset() { + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } + catch (e) { + } + }); + this.restoreHandlers = []; + this.shadowDoms = new WeakSet(); + } +} + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +} + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +/* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ +var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +// Use a lookup table to find the index. +var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); +for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; +} +var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; +}; + +const canvasVarMap = new Map(); +function variableListFor(ctx, ctor) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); +} +const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; +}; +function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + const src = value.toDataURL(); + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; +} +const serializeArgs = (args, win, ctx) => { + return args.map((arg) => serializeArg(arg, win, ctx)); +}; +const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); +}; + +function initCanvas2DMutationObserver(cb, win, blockClass, blockSelector) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { + setTimeout(() => { + const recordArgs = serializeArgs(args, win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function getNormalizedContextName(contextType) { + return contextType === 'experimental-webgl' ? 'webgl' : contextType; +} +function initCanvasContextObserver(win, blockClass, blockSelector, setPreserveDrawingBufferToTrue) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, true)) { + const ctxName = getNormalizedContextName(contextType); + if (!('__context' in this)) + this.__context = ctxName; + if (setPreserveDrawingBufferToTrue && + ['webgl', 'webgl2'].includes(ctxName)) { + if (args[0] && typeof args[0] === 'object') { + const contextAttributes = args[0]; + if (!contextAttributes.preserveDrawingBuffer) { + contextAttributes.preserveDrawingBuffer = true; + } + } + else { + args.splice(0, 1, { + preserveDrawingBuffer: true, + }); + } + } + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function patchGLPrototype(prototype, type, cb, blockClass, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + if ([ + 'isContextLost', + 'canvas', + 'drawingBufferWidth', + 'drawingBufferHeight', + ].includes(prop)) { + continue; + } + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, this); + if ('tagName' in this.canvas && + !isBlocked(this.canvas, blockClass, blockSelector, true)) { + const recordArgs = serializeArgs(args, win, this); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; +} +function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function funcToSource(fn, sourcemapArg) { + var sourcemap = sourcemapArg === undefined ? null : sourcemapArg; + var source = fn.toString(); + var lines = source.split('\n'); + lines.pop(); + lines.shift(); + var blankPrefixLength = lines[0].search(/\S/); + var regex = /(['"])__worker_loader_strict__(['"])/g; + for (var i = 0, n = lines.length; i < n; ++i) { + lines[i] = lines[i].substring(blankPrefixLength).replace(regex, '$1use strict$2') + '\n'; + } + if (sourcemap) { + lines.push('\/\/# sourceMappingURL=' + sourcemap + '\n'); + } + return lines; +} + +function createURL(fn, sourcemapArg) { + var lines = funcToSource(fn, sourcemapArg); + var blob = new Blob(lines, { type: 'application/javascript' }); + return URL.createObjectURL(blob); +} + +function createInlineWorkerFactory(fn, sourcemapArg) { + var url; + return function WorkerFactory(options) { + url = url || createURL(fn, sourcemapArg); + return new Worker(url, options); + }; +} + +var WorkerFactory = createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () { +(function () { + '__worker_loader_strict__'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const lastBlobMap = new Map(); + const transparentBlobMap = new Map(); + function getTransparentBlobFor(width, height, dataURLOptions) { + return __awaiter(this, void 0, void 0, function* () { + const id = `${width}-${height}`; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) + return transparentBlobMap.get(id); + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + transparentBlobMap.set(id, base64); + return base64; + } + else { + return ''; + } + }); + } + const worker = self; + worker.onmessage = function (e) { + return __awaiter(this, void 0, void 0, function* () { + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor(width, height, dataURLOptions); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const type = blob.type; + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + if (!lastBlobMap.has(id) && (yield transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + if (lastBlobMap.get(id) === base64) + return worker.postMessage({ id }); + worker.postMessage({ + id, + type, + base64, + width, + height, + }); + lastBlobMap.set(id, base64); + } + else { + return worker.postMessage({ id: e.data.id }); + } + }); + }; + +})(); +}, null); + +class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = (target, mutation) => { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas, dataURLOptions, } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass, blockSelector); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { + dataURLOptions, + }); + } + initCanvasFPSObserver(fps, win, blockClass, blockSelector, options) { + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, true); + const snapshotInProgressMap = new Map(); + const worker = new WorkerFactory(); + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + if (!('base64' in e.data)) + return; + const { base64, type, width, height } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', + args: [0, 0, width, height], + }, + { + property: 'drawImage', + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + }, + 0, + 0, + ], + }, + ], + }); + }; + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId; + const getCanvas = () => { + const matchedCanvas = []; + win.document.querySelectorAll('canvas').forEach((canvas) => { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { + matchedCanvas.push(canvas); + } + }); + return matchedCanvas; + }; + const takeCanvasSnapshots = (timestamp) => { + if (lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + getCanvas() + .forEach((canvas) => __awaiter(this, void 0, void 0, function* () { + var _a; + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) + return; + if (canvas.width === 0 || canvas.height === 0) + return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes(canvas.__context)) { + const context = canvas.getContext(canvas.__context); + if (((_a = context === null || context === void 0 ? void 0 : context.getContextAttributes()) === null || _a === void 0 ? void 0 : _a.preserveDrawingBuffer) === false) { + context.clear(context.COLOR_BUFFER_BIT); + } + } + const bitmap = yield createImageBitmap(canvas); + worker.postMessage({ + id, + bitmap, + width: canvas.width, + height: canvas.height, + dataURLOptions: options.dataURLOptions, + }, [bitmap]); + })); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + rafId = requestAnimationFrame(takeCanvasSnapshots); + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + initCanvasMutationObserver(win, blockClass, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, false); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } +} + +class StylesheetManager { + constructor(options) { + this.trackedLinkElements = new WeakSet(); + this.styleMirror = new StyleSheetMirror(); + this.mutationCb = options.mutationCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; + } + attachLinkElement(linkEl, childSn) { + if ('_cssText' in childSn.attributes) + this.mutationCb({ + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: childSn.id, + attributes: childSn + .attributes, + }, + ], + }); + this.trackLinkElement(linkEl); + } + trackLinkElement(linkEl) { + if (this.trackedLinkElements.has(linkEl)) + return; + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + adoptStyleSheets(sheets, hostId) { + if (sheets.length === 0) + return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [], + }; + const styles = []; + for (const sheet of sheets) { + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + styles.push({ + styleId, + rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ + rule: stringifyRule(r), + index, + })), + }); + } + else + styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); + } + if (styles.length > 0) + adoptedStyleSheetData.styles = styles; + this.adoptedStyleSheetCb(adoptedStyleSheetData); + } + reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + trackStylesheetInLinkElement(linkEl) { + } +} + +class ProcessedNodeManager { + constructor() { + this.nodeMap = new WeakMap(); + this.loop = true; + this.periodicallyClear(); + } + periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + if (this.loop) + this.periodicallyClear(); + }); + } + inOtherBuffer(node, thisBuffer) { + const buffers = this.nodeMap.get(node); + return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + } + add(node, buffer) { + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + } + clear() { + this.nodeMap = new WeakMap(); + } + destroy() { + this.loop = false; + } +} + +function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: nowTimestamp() }); +} +let wrappedEmit; +let takeFullSnapshot; +let canvasManager; +let recording = false; +const mirror = createMirror(); +function record(options = {}) { + const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordDOM = true, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded' + ? options.recordAfter + : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; + registerErrorHandler(errorHandler); + const inEmittingFrame = recordCrossOriginIframes + ? window.parent === window + : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + if (window.parent.document) { + passEmitsToParent = false; + } + } + catch (e) { + passEmitsToParent = true; + } + } + if (inEmittingFrame && !emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + mirror.reset(); + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn && + !passEmitsToParent) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType$1.FullSnapshot && + !(e.type === EventType$1.IncrementalSnapshot && + e.data.source === IncrementalSource$1.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + if (inEmittingFrame) { + emit === null || emit === void 0 ? void 0 : emit(eventProcessor(e), isCheckout); + } + else if (passEmitsToParent) { + const message = { + type: 'rrweb', + event: eventProcessor(e), + origin: window.location.origin, + isCheckout, + }; + window.parent.postMessage(message, '*'); + } + if (e.type === EventType$1.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType$1.IncrementalSnapshot) { + if (e.data.source === IncrementalSource$1.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p), + })); + const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a), + })); + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, + }); + const iframeManager = new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + }); + } + const processedNodeManager = new ProcessedNodeManager(); + canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + if (!recordDOM) { + return; + } + wrappedEmit(wrapEvent({ + type: EventType$1.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + stylesheetManager.reset(); + shadowDomManager.init(); + mutationBuffers.forEach((buf) => buf.lock()); + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + dataURLOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.trackLinkElement(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachLinkElement(linkEl, childSn); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.FullSnapshot, + data: { + node, + initialOffset: getWindowScroll(window), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.unlock()); + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document)); + }; + try { + const handlers = []; + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Font }, p), + })), + selectionCb: (p) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Selection }, p), + })); + }, + customElementCb: (c) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CustomElement }, c), + })); + }, + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordDOM, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + dataURLOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + processedNodeManager, + canvasManager, + ignoreCSSAttributes, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType$1.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + recording = true; + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.DomContentLoaded, + data: {}, + })); + if (recordAfter === 'DOMContentLoaded') + init(); + })); + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.Load, + data: {}, + })); + if (recordAfter === 'load') + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + processedNodeManager.destroy(); + recording = false; + unregisterErrorHandler(); + }; + } + catch (error) { + console.warn(error); + } +} +record.addCustomEvent = (tag, payload) => { + if (!recording) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.Custom, + data: { + tag, + payload, + }, + })); +}; +record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); +}; +record.takeFullSnapshot = (isCheckout) => { + if (!recording) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); +}; +record.mirror = mirror; + +var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; +})(EventType || {}); +var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; +})(IncrementalSource || {}); + var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -63,7 +4570,7 @@ var _ = { }; // Console override -var console = { +var console$1 = { /** @type {function(...*)} */ log: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { @@ -120,14 +4627,14 @@ var console = { var log_func_with_prefix = function(func, prefix) { return function() { arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); + return func.apply(console$1, arguments); }; }; var console_with_prefix = function(prefix) { return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) + log: log_func_with_prefix(console$1.log, prefix), + error: log_func_with_prefix(console$1.error, prefix), + critical: log_func_with_prefix(console$1.critical, prefix) }; }; @@ -980,7 +5487,7 @@ _.getQueryParam = function(url, param) { try { result = decodeURIComponent(result); } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); + console$1.error('Skipping decoding for malformed query param: ' + result); } return result.replace(/\+/g, ' '); } @@ -1107,13 +5614,13 @@ _.localStorage = { is_supported: function(force_check) { var supported = localStorageSupported(null, force_check); if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); + console$1.error('localStorage unsupported; falling back to cookie store'); } return supported; }, error: function(msg) { - console.error('localStorage error: ' + msg); + console$1.error('localStorage error: ' + msg); }, get: function(name) { @@ -1168,7 +5675,7 @@ _.register_event = (function() { */ var register_event = function(element, type, handler, oldSchool, useCapture) { if (!element) { - console.error('No valid element provided to register_event'); + console$1.error('No valid element provided to register_event'); return; } @@ -1730,158 +6237,306 @@ _['info']['browser'] = _.info.browser; _['info']['browserVersion'] = _.info.browserVersion; _['info']['properties'] = _.info.properties; -/* eslint camelcase: "off" */ - /** - * DomTracker Object - * @constructor + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. */ -var DomTracker = function() {}; - - -// interface -DomTracker.prototype.create_properties = function() {}; -DomTracker.prototype.event_handler = function() {}; -DomTracker.prototype.after_track_handler = function() {}; - -DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; -}; /** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ -DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - that.event_handler(e, this, options); +/** Public **/ - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); +var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); +/** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function optIn(token, options) { + _optInOut(true, token, options); +} - return true; -}; +/** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ +function optOut(token, options) { + _optInOut(false, token, options); +} /** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type */ -DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; +function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; +} - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; +/** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ +function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console$1.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console$1.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; +} - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } +/** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); +} - that.after_track_handler(props, options, timeout_occured); - }; -}; +/** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} -DomTracker.prototype.create_properties = function(properties, element) { - var props; +/** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ +function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); +} - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } +/** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); +} - return props; -}; +/** Private **/ /** - * LinkTracker Object - * @constructor - * @extends DomTracker + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage */ -var LinkTracker = function() { - this.override_event = 'click'; -}; -_.inherit(LinkTracker, DomTracker); +function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; +} -LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); +/** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ +function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; +} - if (element.href) { props['url'] = element.href; } +/** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ +function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); +} - return props; -}; +/** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ +function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; -LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); - if (!options.new_tab) { - evt.preventDefault(); + return hasDntOn; +} + +/** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ +function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console$1.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; } -}; -LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } + options = options || {}; - setTimeout(function() { - window.location = options.href; - }, 0); -}; + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } +} /** - * FormTracker Object - * @constructor - * @extends DomTracker + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out */ -var FormTracker = function() { - this.override_event = 'submit'; -}; -_.inherit(FormTracker, DomTracker); +function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; -FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); -}; + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests -FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); -}; + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console$1.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } -var logger$2 = console_with_prefix('lock'); + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; +} + +var logger$3 = console_with_prefix('lock'); /** * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser @@ -1938,7 +6593,7 @@ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { var delay = function(cb) { if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); storage.removeItem(keyZ); storage.removeItem(keyY); loop(); @@ -2027,7 +6682,7 @@ SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { } }; -var logger$1 = console_with_prefix('batch'); +var logger$2 = console_with_prefix('batch'); /** * RequestQueue: queue for batching API requests with localStorage backup for retries. @@ -2049,9 +6704,10 @@ var RequestQueue = function(storageKey, options) { options = options || {}; this.storageKey = storageKey; this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2076,29 +6732,36 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2109,7 +6772,7 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2158,61 +6821,67 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) { _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2240,25 +6909,32 @@ var updatePayloads = function(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } + if (!this.usePersistence) { if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2301,13 +6977,16 @@ RequestQueue.prototype.saveToStorage = function(queue) { */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes -var logger = console_with_prefix('batch'); +var logger$1 = console_with_prefix('batch'); /** * RequestBatcher: manages the queueing, flushing, retry etc of requests of one @@ -2319,7 +6998,8 @@ var RequestBatcher = function(storageKey, options) { this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2336,6 +7016,11 @@ var RequestBatcher = function(storageKey, options) { // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2411,7 +7096,7 @@ RequestBatcher.prototype.flush = function(options) { try { if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); + logger$1.log('Flush: Request already in progress'); return; } @@ -2420,6 +7105,9 @@ RequestBatcher.prototype.flush = function(options) { var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2487,22 +7175,17 @@ RequestBatcher.prototype.flush = function(options) { this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2526,7 +7209,11 @@ RequestBatcher.prototype.flush = function(options) { _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2572,9 +7259,8 @@ RequestBatcher.prototype.flush = function(options) { if (options.unloading) { requestOptions.transport = 'sendBeacon'; } - logger.log('MIXPANEL REQUEST:', dataForRequest); + logger$1.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2585,7 +7271,7 @@ RequestBatcher.prototype.flush = function(options) { * Log error to global logger and optional user-defined logger. */ RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); + logger$1.error.apply(logger$1.error, arguments); if (this.errorReporter) { try { if (!(err instanceof Error)) { @@ -2593,309 +7279,402 @@ RequestBatcher.prototype.reportError = function(msg, err) { } this.errorReporter(msg, err); } catch(err) { - logger.error(err); + logger$1.error(err); } } }; -/** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - -/** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ +var logger = console_with_prefix('recorder'); +var CompressionStream = win['CompressionStream']; -/** Public **/ +var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true +}; -var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; +var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, +]); -/** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function optIn(token, options) { - _optInOut(true, token, options); +function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); } -/** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ -function optOut(token, options) { - _optInOut(false, token, options); -} +var MixpanelRecorder = function(mixpanelInstance) { + this._mixpanel = mixpanelInstance; -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ -function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; -} + // internal rrweb stopRecording function + this._stopRecording = null; -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ -function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; + this.recEvents = []; + this.seqNo = 0; + this.replayId = null; + this.replayStartTime = null; + this.sendBatchId = null; + + this.idleTimeoutId = null; + this.maxTimeoutId = null; + + this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); +}; + + +MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); +}; + +// eslint-disable-next-line camelcase +MixpanelRecorder.prototype.get_config = function(configVar) { + return this._mixpanel.get_config(configVar); +}; + +MixpanelRecorder.prototype.startRecording = function () { + if (this._stopRecording !== null) { + logger.log('Recording already in progress, skipping startRecording.'); + return; } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + + this.recordMaxMs = this.get_config('record_max_ms'); + if (this.recordMaxMs > MAX_RECORDING_MS) { + this.recordMaxMs = MAX_RECORDING_MS; + logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.'); } - return optedOut; -} -/** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); -} + this.recEvents = []; + this.seqNo = 0; + this.replayStartTime = null; -/** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + this.replayId = _.UUID(); + + this.batcher.start(); + + var resetIdleTimeout = _.bind(function () { + clearTimeout(this.idleTimeoutId); + this.idleTimeoutId = setTimeout(_.bind(function () { + logger.log('Idle timeout reached, restarting recording.'); + this.resetRecording(); + }, this), this.get_config('record_idle_timeout_ms')); + }, this); + + this._stopRecording = record({ + 'emit': _.bind(function (ev) { + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } + }, this), + 'blockClass': this.get_config('record_block_class'), + 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, + 'maskTextClass': this.get_config('record_mask_text_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); -} + + resetIdleTimeout(); + + this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); +}; + +MixpanelRecorder.prototype.resetRecording = function () { + this.stopRecording(); + this.startRecording(); +}; + +MixpanelRecorder.prototype.stopRecording = function () { + if (this._stopRecording !== null) { + this._stopRecording(); + this._stopRecording = null; + } + + this.batcher.flush(); // flush any remaining events + this.replayId = null; + + clearTimeout(this.idleTimeoutId); + clearTimeout(this.maxTimeoutId); +}; /** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out + * Flushes the current batch of events to the server, but passes an opt-out callback to make sure + * we stop recording and dump any queued events if the user has opted out. */ -function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); +MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); +}; + +MixpanelRecorder.prototype._onOptOut = function (code) { + // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out + if (code === 0) { + this.recEvents = []; + this.stopRecording(); + } +}; + +MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/octet-stream' + }, + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); -} +}; -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); -} +MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + + var reqParams = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + var eventsJson = _.JSONEncode(data); + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqParams['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqParams['$user_id'] = userId; + } + + if (CompressionStream) { + var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); + var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); + new Response(gzipStream) + .blob() + .then(_.bind(function(compressedBlob) { + reqParams['format'] = 'gzip'; + this._sendRequest(reqParams, compressedBlob, callback); + }, this)); + } else { + reqParams['format'] = 'body'; + this._sendRequest(reqParams, eventsJson, callback); + } + } +}); + + +MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } +}; -/** Private **/ -/** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ -function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; -} +win['__mp_recorder'] = MixpanelRecorder; -/** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ -function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; -} +/* eslint camelcase: "off" */ /** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type + * DomTracker Object + * @constructor */ -function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); -} +var DomTracker = function() {}; -/** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ -function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); +// interface +DomTracker.prototype.create_properties = function() {}; +DomTracker.prototype.event_handler = function() {}; +DomTracker.prototype.after_track_handler = function() {}; - return hasDntOn; -} +DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; +}; /** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback */ -function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); +DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); return; } - options = options || {}; + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); + that.event_handler(e, this, options); - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); }); - } -} + }, this); + + return true; +}; /** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured */ -function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; +DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; } - if (!optedOut) { - return method.apply(this, arguments); - } + that.after_track_handler(props, options, timeout_occured); + }; +}; - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } +DomTracker.prototype.create_properties = function(properties, element) { + var props; - return; - }; -} + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; +}; + +/** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ +var LinkTracker = function() { + this.override_event = 'click'; +}; +_.inherit(LinkTracker, DomTracker); + +LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; +}; + +LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } +}; + +LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); +}; + +/** + * FormTracker Object + * @constructor + * @extends DomTracker + */ +var FormTracker = function() { + this.override_event = 'submit'; +}; +_.inherit(FormTracker, DomTracker); + +FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); +}; + +FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); +}; /* eslint camelcase: "off" */ @@ -3317,7 +8096,7 @@ MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); return; } else { $add[k] = v; @@ -3442,7 +8221,7 @@ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(am if (!_.isNumber(amount)) { amount = parseFloat(amount); if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } @@ -3479,7 +8258,7 @@ MixpanelPeople.prototype.clear_charges = function(callback) { */ MixpanelPeople.prototype.delete_user = function() { if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); return; } var data = {'$delete': this._mixpanel.get_distinct_id()}; @@ -3553,7 +8332,7 @@ MixpanelPeople.prototype._enqueue = function(data) { } else if (UNION_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); } else { - console.error('Invalid call to _enqueue():', data); + console$1.error('Invalid call to _enqueue():', data); } }; @@ -3701,7 +8480,7 @@ var MixpanelPersistence = function(config) { var storage_type = config['persistence']; if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); storage_type = config['persistence'] = 'cookie'; } @@ -4005,8 +8784,8 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { this._pop_from_people_queue(UNSET_ACTION, q_data); } - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); this.save(); }; @@ -4051,7 +8830,7 @@ MixpanelPersistence.prototype._get_queue_key = function(queue) { } else if (queue === UNION_ACTION) { return UNION_QUEUE_KEY; } else { - console.error('Invalid queue:', queue); + console$1.error('Invalid queue:', queue); } }; @@ -4108,6 +8887,12 @@ Globals should be all caps */ var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -4201,7 +8986,9 @@ var DEFAULT_CONFIG = { 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4234,7 +9021,7 @@ var create_mplib = function(token, config, name) { instance = target; } else { if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); + console$1.error('You have already initialized ' + name); return; } instance = new MixpanelLib(); @@ -4362,9 +9149,9 @@ MixpanelLib.prototype._init = function(token, config, name) { if (this._batch_requests) { if (!_.localStorage.is_supported(true) || !USE_XHR) { this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); + console$1.log('Clearing batch queue ' + batcher_config.queue_key); _.localStorage.remove(batcher_config.queue_key); }); } else { @@ -4427,7 +9214,7 @@ MixpanelLib.prototype._init = function(token, config, name) { MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); + console$1.critical('Browser does not support MutationObserver; skipping session recording'); return; } @@ -4437,12 +9224,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(functi }, this); if (_.isUndefined(win['__mp_recorder'])) { - var scriptEl = document$1.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document$1.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -4452,7 +9234,7 @@ MixpanelLib.prototype.stop_session_recording = function () { if (this._recorder) { this._recorder['stopRecording'](); } else { - console.critical('Session recorder module not loaded'); + console$1.critical('Session recorder module not loaded'); } }; @@ -4741,7 +9523,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4841,6 +9624,7 @@ MixpanelLib.prototype.init_batchers = function() { attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4852,8 +9636,8 @@ MixpanelLib.prototype.init_batchers = function() { beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); @@ -4945,8 +9729,8 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) { truncated_data = this._run_hook('before_send_' + options.type, truncated_data); } if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); return this._send_request( endpoint, this._encode_data_for_request(truncated_data), @@ -6143,14 +10927,14 @@ MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { }; MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); + console$1.error.apply(console$1.error, arguments); try { if (!err && !(msg instanceof Error)) { msg = new Error(msg); } this.get_config('error_reporter')(msg, err); } catch(err) { - console.error(err); + console$1.error(err); } }; @@ -6302,7 +11086,8 @@ var add_dom_loaded_handler = function() { _.register_event(win, 'load', dom_loaded_handler, true); }; -function init_as_module() { +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); @@ -6313,9 +11098,16 @@ function init_as_module() { return mixpanel_master; } +// For loading separate bundles asynchronously via script tag + +// For builds that have everything in one bundle, no extra work. +function loadNoop (_src, onload) { + onload(); +} + /* eslint camelcase: "off" */ -var mixpanel = init_as_module(); +var mixpanel = init_as_module(loadNoop); module.exports = mixpanel; diff --git a/examples/es2015-babelify/bundle.js b/examples/es2015-babelify/bundle.js index eaa0a212..e0aca8ba 100644 --- a/examples/es2015-babelify/bundle.js +++ b/examples/es2015-babelify/bundle.js @@ -17,7 +17,10826 @@ _srcLoadersLoaderModule2['default'].init("FAKE_TOKEN", { _srcLoadersLoaderModule2['default'].track('Tracking after mixpanel.init'); -},{"../../src/loaders/loader-module":6}],2:[function(require,module,exports){ +},{"../../src/loaders/loader-module":9}],2:[function(require,module,exports){ +(function(global, factory) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.rrwebTypes = {})); +})(this, function(exports2) { + "use strict"; + var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType || {}); + var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource || {}); + var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; + })(MouseInteractions || {}); + var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; + })(PointerTypes || {}); + var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; + })(CanvasContext || {}); + var MediaInteractions = /* @__PURE__ */ ((MediaInteractions2) => { + MediaInteractions2[MediaInteractions2["Play"] = 0] = "Play"; + MediaInteractions2[MediaInteractions2["Pause"] = 1] = "Pause"; + MediaInteractions2[MediaInteractions2["Seeked"] = 2] = "Seeked"; + MediaInteractions2[MediaInteractions2["VolumeChange"] = 3] = "VolumeChange"; + MediaInteractions2[MediaInteractions2["RateChange"] = 4] = "RateChange"; + return MediaInteractions2; + })(MediaInteractions || {}); + var ReplayerEvents = /* @__PURE__ */ ((ReplayerEvents2) => { + ReplayerEvents2["Start"] = "start"; + ReplayerEvents2["Pause"] = "pause"; + ReplayerEvents2["Resume"] = "resume"; + ReplayerEvents2["Resize"] = "resize"; + ReplayerEvents2["Finish"] = "finish"; + ReplayerEvents2["FullsnapshotRebuilded"] = "fullsnapshot-rebuilded"; + ReplayerEvents2["LoadStylesheetStart"] = "load-stylesheet-start"; + ReplayerEvents2["LoadStylesheetEnd"] = "load-stylesheet-end"; + ReplayerEvents2["SkipStart"] = "skip-start"; + ReplayerEvents2["SkipEnd"] = "skip-end"; + ReplayerEvents2["MouseInteraction"] = "mouse-interaction"; + ReplayerEvents2["EventCast"] = "event-cast"; + ReplayerEvents2["CustomEvent"] = "custom-event"; + ReplayerEvents2["Flush"] = "flush"; + ReplayerEvents2["StateChange"] = "state-change"; + ReplayerEvents2["PlayBack"] = "play-back"; + ReplayerEvents2["Destroy"] = "destroy"; + return ReplayerEvents2; + })(ReplayerEvents || {}); + exports2.CanvasContext = CanvasContext; + exports2.EventType = EventType; + exports2.IncrementalSource = IncrementalSource; + exports2.MediaInteractions = MediaInteractions; + exports2.MouseInteractions = MouseInteractions; + exports2.PointerTypes = PointerTypes; + exports2.ReplayerEvents = ReplayerEvents; + Object.defineProperties(exports2, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); +}); + + +},{}],3:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var NodeType$2; +(function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; +})(NodeType$2 || (NodeType$2 = {})); + +function isElement(n) { + return n.nodeType === n.ELEMENT_NODE; +} +function isShadowRoot(n) { + const host = n === null || n === void 0 ? void 0 : n.host; + return Boolean((host === null || host === void 0 ? void 0 : host.shadowRoot) === n); +} +function isNativeShadowDom(shadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; +} +function fixBrowserCompatibilityIssuesInCSS(cssText) { + if (cssText.includes(' background-clip: text;') && + !cssText.includes(' -webkit-background-clip: text;')) { + cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;'); + } + return cssText; +} +function escapeImportStatement(rule) { + const { cssText } = rule; + if (cssText.split('"').length < 3) + return cssText; + const statement = ['@import', `url(${JSON.stringify(rule.href)})`]; + if (rule.layerName === '') { + statement.push(`layer`); + } + else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); + } + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); + } + if (rule.media.length) { + statement.push(rule.media.mediaText); + } + return statement.join(' ') + ';'; +} +function stringifyStylesheet(s) { + try { + const rules = s.rules || s.cssRules; + return rules + ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) + : null; + } + catch (error) { + return null; + } +} +function stringifyRule(rule) { + let importStringified; + if (isCSSImportRule(rule)) { + try { + importStringified = + stringifyStylesheet(rule.styleSheet) || + escapeImportStatement(rule); + } + catch (error) { + } + } + else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + return fixSafariColons(rule.cssText); + } + return importStringified || rule.cssText; +} +function fixSafariColons(cssStringified) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); +} +function isCSSImportRule(rule) { + return 'styleSheet' in rule; +} +function isCSSStyleRule(rule) { + return 'selectorText' in rule; +} +class Mirror$2 { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} +function createMirror$2() { + return new Mirror$2(); +} +function maskInputValue({ element, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + const actualType = type && toLowerCase(type); + if (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType])) { + if (maskInputFn) { + text = maskInputFn(text, element); + } + else { + text = '*'.repeat(text.length); + } + } + return text; +} +function toLowerCase(str) { + return str.toLowerCase(); +} +const ORIGINAL_ATTRIBUTE_NAME$1 = '__rrweb_original__'; +function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME$1 in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME$1] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; +} +function isNodeMetaEqual(a, b) { + if (!a || !b || a.type !== b.type) + return false; + if (a.type === NodeType$2.Document) + return a.compatMode === b.compatMode; + else if (a.type === NodeType$2.DocumentType) + return (a.name === b.name && + a.publicId === b.publicId && + a.systemId === b.systemId); + else if (a.type === NodeType$2.Comment || + a.type === NodeType$2.Text || + a.type === NodeType$2.CDATA) + return a.textContent === b.textContent; + else if (a.type === NodeType$2.Element) + return (a.tagName === b.tagName && + JSON.stringify(a.attributes) === + JSON.stringify(b.attributes) && + a.isSVG === b.isSVG && + a.needBlock === b.needBlock); + return false; +} +function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? + toLowerCase(type) + : null; +} +function extractFileExtension(path, baseURL) { + var _a; + let url; + try { + url = new URL(path, baseURL !== null && baseURL !== void 0 ? baseURL : window.location.href); + } + catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; + const match = url.pathname.match(regex); + return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null; +} + +let _id = 1; +const tagNameRegex = new RegExp('[^a-z0-9-_:]'); +const IGNORED_NODE = -2; +function genId() { + return _id++; +} +function getValidTagName$1(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; +} +function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; +} +let canvasService; +let canvasCtx; +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; +const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; +const URL_WWW_MATCH = /^www\..*/i; +const DATA_URI = /^(data:)([^,]*),(.*)/i; +function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); +} +const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; +const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; +function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + const output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); +} +function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; +} +function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); +} +function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; +} +function transformAttribute(doc, tagName, name, value) { + if (!value) { + return value; + } + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + return value; +} +function ignoreAttribute(tagName, name, _value) { + return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; +} +function _isBlockedElement(element, blockClass, blockSelector) { + try { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = element.classList.length; eIndex--;) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + } + catch (e) { + } + return false; +} +function classMatchesRegex(node, regex, checkAncestors) { + if (!node) + return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + for (let eIndex = node.classList.length; eIndex--;) { + const className = node.classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); +} +function needMaskingText(node, maskTextClass, maskTextSelector, checkAncestors) { + try { + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + if (el === null) + return false; + if (typeof maskTextClass === 'string') { + if (checkAncestors) { + if (el.closest(`.${maskTextClass}`)) + return true; + } + else { + if (el.classList.contains(maskTextClass)) + return true; + } + } + else { + if (classMatchesRegex(el, maskTextClass, checkAncestors)) + return true; + } + if (maskTextSelector) { + if (checkAncestors) { + if (el.closest(maskTextSelector)) + return true; + } + else { + if (el.matches(maskTextSelector)) + return true; + } + } + } + catch (e) { + } + return false; +} +function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); + } + iframeEl.addEventListener('load', listener); +} +function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; + } + catch (error) { + return; + } + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); +} +function serializeNode(n, options) { + const { doc, mirror, blockClass, blockSelector, needsMask, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options; + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType$2.Document, + childNodes: [], + compatMode: n.compatMode, + }; + } + else { + return { + type: NodeType$2.Document, + childNodes: [], + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType$2.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n, { + needsMask, + maskTextFn, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType$2.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType$2.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } +} +function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; +} +function serializeTextNode(n, options) { + var _a; + const { needsMask, maskTextFn, rootId } = options; + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStylesheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (!isStyle && !isScript && textContent && needsMask) { + textContent = maskTextFn + ? maskTextFn(textContent, n.parentElement) + : textContent.replace(/[\S]/g, '*'); + } + return { + type: NodeType$2.Text, + textContent: textContent || '', + isStyle, + rootId, + }; +} +function serializeElementNode(n, options) { + const { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions = {}, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, } = options; + const needBlock = _isBlockedElement(n, blockClass, blockSelector); + const tagName = getValidTagName$1(n); + let attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = stringifyStylesheet(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = n.value; + const checked = n.checked; + if (attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value) { + attributes.value = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } + else if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + image.removeEventListener('load', recordInlineImage); + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.addEventListener('load', recordInlineImage); + } + if (tagName === 'audio' || tagName === 'video') { + const mediaAttributes = attributes; + mediaAttributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + mediaAttributes.rr_mediaCurrentTime = n.currentTime; + mediaAttributes.rr_mediaPlaybackRate = n.playbackRate; + mediaAttributes.rr_mediaMuted = n.muted; + mediaAttributes.rr_mediaLoop = n.loop; + mediaAttributes.rr_mediaVolume = n.volume; + } + if (!newlyAddedElement) { + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + let isCustomElement; + try { + if (customElements.get(tagName)) + isCustomElement = true; + } + catch (e) { + } + return { + type: NodeType$2.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + isCustom: isCustomElement, + }; +} +function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } +} +function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType$2.Comment) { + return true; + } + else if (sn.type === NodeType$2.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + extractFileExtension(sn.attributes.href) === 'js'))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; +} +function serializeNodeWithId(n, options) { + const { doc, mirror, blockClass, blockSelector, maskTextClass, maskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options; + let { needsMask } = options; + let { preserveWhiteSpace = true } = options; + if (!needsMask && + n.childNodes) { + const checkAncestors = needsMask === undefined; + needsMask = needMaskingText(n, maskTextClass, maskTextSelector, checkAncestors); + } + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if (mirror.hasNode(n)) { + id = mirror.getId(n); + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType$2.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + mirror.add(n, serializedNode); + if (id === IGNORED_NODE) { + return null; + } + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType$2.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + const shadowRoot = n.shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType$2.Document || + serializedNode.type === NodeType$2.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + serializedNode.type === NodeType$2.Element && + serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }; + if (serializedNode.type === NodeType$2.Element && + serializedNode.tagName === 'textarea' && + serializedNode.attributes.value !== undefined) ; + else { + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType$2.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + if (serializedNode.type === NodeType$2.Element && + serializedNode.tagName === 'link' && + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css'))) { + onceStylesheetLoaded(n, () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedLinkNode) { + onStylesheetLoad(n, serializedLinkNode); + } + } + }, stylesheetLoadTimeout); + } + return serializedNode; +} +function snapshot(n, options) { + const { mirror = new Mirror$2(), blockClass = 'rr-block', blockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + }); +} + +const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; +function parse(css, options = {}) { + let lineno = 1; + let column = 1; + function updatePosition(str) { + const lines = str.match(/\n/g); + if (lines) { + lineno += lines.length; + } + const i = str.lastIndexOf('\n'); + column = i === -1 ? column + str.length : str.length - i; + } + function position() { + const start = { line: lineno, column }; + return (node) => { + node.position = new Position(start); + whitespace(); + return node; + }; + } + class Position { + constructor(start) { + this.start = start; + this.end = { line: lineno, column }; + this.source = options.source; + } + } + Position.prototype.content = css; + const errorsList = []; + function error(msg) { + const err = new Error(`${options.source || ''}:${lineno}:${column}: ${msg}`); + err.reason = msg; + err.filename = options.source; + err.line = lineno; + err.column = column; + err.source = css; + if (options.silent) { + errorsList.push(err); + } + else { + throw err; + } + } + function stylesheet() { + const rulesList = rules(); + return { + type: 'stylesheet', + stylesheet: { + source: options.source, + rules: rulesList, + parsingErrors: errorsList, + }, + }; + } + function open() { + return match(/^{\s*/); + } + function close() { + return match(/^}/); + } + function rules() { + let node; + const rules = []; + whitespace(); + comments(rules); + while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) { + if (node) { + rules.push(node); + comments(rules); + } + } + return rules; + } + function match(re) { + const m = re.exec(css); + if (!m) { + return; + } + const str = m[0]; + updatePosition(str); + css = css.slice(str.length); + return m; + } + function whitespace() { + match(/^\s*/); + } + function comments(rules = []) { + let c; + while ((c = comment())) { + if (c) { + rules.push(c); + } + c = comment(); + } + return rules; + } + function comment() { + const pos = position(); + if ('/' !== css.charAt(0) || '*' !== css.charAt(1)) { + return; + } + let i = 2; + while ('' !== css.charAt(i) && + ('*' !== css.charAt(i) || '/' !== css.charAt(i + 1))) { + ++i; + } + i += 2; + if ('' === css.charAt(i - 1)) { + return error('End of comment missing'); + } + const str = css.slice(2, i - 2); + column += 2; + updatePosition(str); + css = css.slice(i); + column += 2; + return pos({ + type: 'comment', + comment: str, + }); + } + function selector() { + whitespace(); + while (css[0] == '}') { + error('extra closing bracket'); + css = css.slice(1); + whitespace(); + } + const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); + if (!m) { + return; + } + const cleanedInput = m[0] + .trim() + .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') + .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => { + return m.replace(/,/g, '\u200C'); + }); + return customSplit(cleanedInput).map((s) => s.replace(/\u200C/g, ',').trim()); + } + function customSplit(input) { + const result = []; + let currentSegment = ''; + let depthParentheses = 0; + let depthBrackets = 0; + for (const char of input) { + if (char === '(') { + depthParentheses++; + } + else if (char === ')') { + depthParentheses--; + } + else if (char === '[') { + depthBrackets++; + } + else if (char === ']') { + depthBrackets--; + } + if (char === ',' && depthParentheses === 0 && depthBrackets === 0) { + result.push(currentSegment); + currentSegment = ''; + } + else { + currentSegment += char; + } + } + if (currentSegment) { + result.push(currentSegment); + } + return result; + } + function declaration() { + const pos = position(); + const propMatch = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/); + if (!propMatch) { + return; + } + const prop = trim(propMatch[0]); + if (!match(/^:\s*/)) { + return error(`property missing ':'`); + } + const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/); + const ret = pos({ + type: 'declaration', + property: prop.replace(commentre, ''), + value: val ? trim(val[0]).replace(commentre, '') : '', + }); + match(/^[;\s]*/); + return ret; + } + function declarations() { + const decls = []; + if (!open()) { + return error(`missing '{'`); + } + comments(decls); + let decl; + while ((decl = declaration())) { + if (decl !== false) { + decls.push(decl); + comments(decls); + } + decl = declaration(); + } + if (!close()) { + return error(`missing '}'`); + } + return decls; + } + function keyframe() { + let m; + const vals = []; + const pos = position(); + while ((m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/))) { + vals.push(m[1]); + match(/^,\s*/); + } + if (!vals.length) { + return; + } + return pos({ + type: 'keyframe', + values: vals, + declarations: declarations(), + }); + } + function atkeyframes() { + const pos = position(); + let m = match(/^@([-\w]+)?keyframes\s*/); + if (!m) { + return; + } + const vendor = m[1]; + m = match(/^([-\w]+)\s*/); + if (!m) { + return error('@keyframes missing name'); + } + const name = m[1]; + if (!open()) { + return error(`@keyframes missing '{'`); + } + let frame; + let frames = comments(); + while ((frame = keyframe())) { + frames.push(frame); + frames = frames.concat(comments()); + } + if (!close()) { + return error(`@keyframes missing '}'`); + } + return pos({ + type: 'keyframes', + name, + vendor, + keyframes: frames, + }); + } + function atsupports() { + const pos = position(); + const m = match(/^@supports *([^{]+)/); + if (!m) { + return; + } + const supports = trim(m[1]); + if (!open()) { + return error(`@supports missing '{'`); + } + const style = comments().concat(rules()); + if (!close()) { + return error(`@supports missing '}'`); + } + return pos({ + type: 'supports', + supports, + rules: style, + }); + } + function athost() { + const pos = position(); + const m = match(/^@host\s*/); + if (!m) { + return; + } + if (!open()) { + return error(`@host missing '{'`); + } + const style = comments().concat(rules()); + if (!close()) { + return error(`@host missing '}'`); + } + return pos({ + type: 'host', + rules: style, + }); + } + function atmedia() { + const pos = position(); + const m = match(/^@media *([^{]+)/); + if (!m) { + return; + } + const media = trim(m[1]); + if (!open()) { + return error(`@media missing '{'`); + } + const style = comments().concat(rules()); + if (!close()) { + return error(`@media missing '}'`); + } + return pos({ + type: 'media', + media, + rules: style, + }); + } + function atcustommedia() { + const pos = position(); + const m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/); + if (!m) { + return; + } + return pos({ + type: 'custom-media', + name: trim(m[1]), + media: trim(m[2]), + }); + } + function atpage() { + const pos = position(); + const m = match(/^@page */); + if (!m) { + return; + } + const sel = selector() || []; + if (!open()) { + return error(`@page missing '{'`); + } + let decls = comments(); + let decl; + while ((decl = declaration())) { + decls.push(decl); + decls = decls.concat(comments()); + } + if (!close()) { + return error(`@page missing '}'`); + } + return pos({ + type: 'page', + selectors: sel, + declarations: decls, + }); + } + function atdocument() { + const pos = position(); + const m = match(/^@([-\w]+)?document *([^{]+)/); + if (!m) { + return; + } + const vendor = trim(m[1]); + const doc = trim(m[2]); + if (!open()) { + return error(`@document missing '{'`); + } + const style = comments().concat(rules()); + if (!close()) { + return error(`@document missing '}'`); + } + return pos({ + type: 'document', + document: doc, + vendor, + rules: style, + }); + } + function atfontface() { + const pos = position(); + const m = match(/^@font-face\s*/); + if (!m) { + return; + } + if (!open()) { + return error(`@font-face missing '{'`); + } + let decls = comments(); + let decl; + while ((decl = declaration())) { + decls.push(decl); + decls = decls.concat(comments()); + } + if (!close()) { + return error(`@font-face missing '}'`); + } + return pos({ + type: 'font-face', + declarations: decls, + }); + } + const atimport = _compileAtrule('import'); + const atcharset = _compileAtrule('charset'); + const atnamespace = _compileAtrule('namespace'); + function _compileAtrule(name) { + const re = new RegExp('^@' + name + '\\s*([^;]+);'); + return () => { + const pos = position(); + const m = match(re); + if (!m) { + return; + } + const ret = { type: name }; + ret[name] = m[1].trim(); + return pos(ret); + }; + } + function atrule() { + if (css[0] !== '@') { + return; + } + return (atkeyframes() || + atmedia() || + atcustommedia() || + atsupports() || + atimport() || + atcharset() || + atnamespace() || + atdocument() || + atpage() || + athost() || + atfontface()); + } + function rule() { + const pos = position(); + const sel = selector(); + if (!sel) { + return error('selector missing'); + } + comments(); + return pos({ + type: 'rule', + selectors: sel, + declarations: declarations(), + }); + } + return addParent(stylesheet()); +} +function trim(str) { + return str ? str.replace(/^\s+|\s+$/g, '') : ''; +} +function addParent(obj, parent) { + const isNode = obj && typeof obj.type === 'string'; + const childParent = isNode ? obj : parent; + for (const k of Object.keys(obj)) { + const value = obj[k]; + if (Array.isArray(value)) { + value.forEach((v) => { + addParent(v, childParent); + }); + } + else if (value && typeof value === 'object') { + addParent(value, childParent); + } + } + if (isNode) { + Object.defineProperty(obj, 'parent', { + configurable: true, + writable: true, + enumerable: false, + value: parent || null, + }); + } + return obj; +} + +const tagMap = { + script: 'noscript', + altglyph: 'altGlyph', + altglyphdef: 'altGlyphDef', + altglyphitem: 'altGlyphItem', + animatecolor: 'animateColor', + animatemotion: 'animateMotion', + animatetransform: 'animateTransform', + clippath: 'clipPath', + feblend: 'feBlend', + fecolormatrix: 'feColorMatrix', + fecomponenttransfer: 'feComponentTransfer', + fecomposite: 'feComposite', + feconvolvematrix: 'feConvolveMatrix', + fediffuselighting: 'feDiffuseLighting', + fedisplacementmap: 'feDisplacementMap', + fedistantlight: 'feDistantLight', + fedropshadow: 'feDropShadow', + feflood: 'feFlood', + fefunca: 'feFuncA', + fefuncb: 'feFuncB', + fefuncg: 'feFuncG', + fefuncr: 'feFuncR', + fegaussianblur: 'feGaussianBlur', + feimage: 'feImage', + femerge: 'feMerge', + femergenode: 'feMergeNode', + femorphology: 'feMorphology', + feoffset: 'feOffset', + fepointlight: 'fePointLight', + fespecularlighting: 'feSpecularLighting', + fespotlight: 'feSpotLight', + fetile: 'feTile', + feturbulence: 'feTurbulence', + foreignobject: 'foreignObject', + glyphref: 'glyphRef', + lineargradient: 'linearGradient', + radialgradient: 'radialGradient', +}; +function getTagName(n) { + let tagName = tagMap[n.tagName] ? tagMap[n.tagName] : n.tagName; + if (tagName === 'link' && n.attributes._cssText) { + tagName = 'style'; + } + return tagName; +} +function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} +const MEDIA_SELECTOR = /(max|min)-device-(width|height)/; +const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g'); +const HOVER_SELECTOR = /([^\\]):hover/; +const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR.source, 'g'); +function adaptCssForReplay(cssText, cache) { + const cachedStyle = cache === null || cache === void 0 ? void 0 : cache.stylesWithHoverClass.get(cssText); + if (cachedStyle) + return cachedStyle; + const ast = parse(cssText, { + silent: true, + }); + if (!ast.stylesheet) { + return cssText; + } + const selectors = []; + const medias = []; + function getSelectors(rule) { + if ('selectors' in rule && rule.selectors) { + rule.selectors.forEach((selector) => { + if (HOVER_SELECTOR.test(selector)) { + selectors.push(selector); + } + }); + } + if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) { + medias.push(rule.media); + } + if ('rules' in rule && rule.rules) { + rule.rules.forEach(getSelectors); + } + } + getSelectors(ast.stylesheet); + let result = cssText; + if (selectors.length > 0) { + const selectorMatcher = new RegExp(selectors + .filter((selector, index) => selectors.indexOf(selector) === index) + .sort((a, b) => b.length - a.length) + .map((selector) => { + return escapeRegExp(selector); + }) + .join('|'), 'g'); + result = result.replace(selectorMatcher, (selector) => { + const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover'); + return `${selector}, ${newSelector}`; + }); + } + if (medias.length > 0) { + const mediaMatcher = new RegExp(medias + .filter((media, index) => medias.indexOf(media) === index) + .sort((a, b) => b.length - a.length) + .map((media) => { + return escapeRegExp(media); + }) + .join('|'), 'g'); + result = result.replace(mediaMatcher, (media) => { + return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2'); + }); + } + cache === null || cache === void 0 ? void 0 : cache.stylesWithHoverClass.set(cssText, result); + return result; +} +function createCache() { + const stylesWithHoverClass = new Map(); + return { + stylesWithHoverClass, + }; +} +function buildNode(n, options) { + var _a; + const { doc, hackCss, cache } = options; + switch (n.type) { + case NodeType$2.Document: + return doc.implementation.createDocument(null, '', null); + case NodeType$2.DocumentType: + return doc.implementation.createDocumentType(n.name || 'html', n.publicId, n.systemId); + case NodeType$2.Element: { + const tagName = getTagName(n); + let node; + if (n.isSVG) { + node = doc.createElementNS('http://www.w3.org/2000/svg', tagName); + } + else { + if (n.isCustom && + ((_a = doc.defaultView) === null || _a === void 0 ? void 0 : _a.customElements) && + !doc.defaultView.customElements.get(n.tagName)) + doc.defaultView.customElements.define(n.tagName, class extends doc.defaultView.HTMLElement { + }); + node = doc.createElement(tagName); + } + const specialAttributes = {}; + for (const name in n.attributes) { + if (!Object.prototype.hasOwnProperty.call(n.attributes, name)) { + continue; + } + let value = n.attributes[name]; + if (tagName === 'option' && + name === 'selected' && + value === false) { + continue; + } + if (value === null) { + continue; + } + if (value === true) + value = ''; + if (name.startsWith('rr_')) { + specialAttributes[name] = value; + continue; + } + const isTextarea = tagName === 'textarea' && name === 'value'; + const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText'; + if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') { + value = adaptCssForReplay(value, cache); + } + if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') { + node.appendChild(doc.createTextNode(value)); + n.childNodes = []; + continue; + } + try { + if (n.isSVG && name === 'xlink:href') { + node.setAttributeNS('http://www.w3.org/1999/xlink', name, value.toString()); + } + else if (name === 'onload' || + name === 'onclick' || + name.substring(0, 7) === 'onmouse') { + node.setAttribute('_' + name, value.toString()); + } + else if (tagName === 'meta' && + n.attributes['http-equiv'] === 'Content-Security-Policy' && + name === 'content') { + node.setAttribute('csp-content', value.toString()); + continue; + } + else if (tagName === 'link' && + (n.attributes.rel === 'preload' || + n.attributes.rel === 'modulepreload') && + n.attributes.as === 'script') { + } + else if (tagName === 'link' && + n.attributes.rel === 'prefetch' && + typeof n.attributes.href === 'string' && + n.attributes.href.endsWith('.js')) { + } + else if (tagName === 'img' && + n.attributes.srcset && + n.attributes.rr_dataURL) { + node.setAttribute('rrweb-original-srcset', n.attributes.srcset); + } + else { + node.setAttribute(name, value.toString()); + } + } + catch (error) { + } + } + for (const name in specialAttributes) { + const value = specialAttributes[name]; + if (tagName === 'canvas' && name === 'rr_dataURL') { + const image = document.createElement('img'); + image.onload = () => { + const ctx = node.getContext('2d'); + if (ctx) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } + }; + image.src = value.toString(); + if (node.RRNodeType) + node.rr_dataURL = value.toString(); + } + else if (tagName === 'img' && name === 'rr_dataURL') { + const image = node; + if (!image.currentSrc.startsWith('data:')) { + image.setAttribute('rrweb-original-src', n.attributes.src); + image.src = value.toString(); + } + } + if (name === 'rr_width') { + node.style.width = value.toString(); + } + else if (name === 'rr_height') { + node.style.height = value.toString(); + } + else if (name === 'rr_mediaCurrentTime' && + typeof value === 'number') { + node.currentTime = value; + } + else if (name === 'rr_mediaState') { + switch (value) { + case 'played': + node + .play() + .catch((e) => console.warn('media playback error', e)); + break; + case 'paused': + node.pause(); + break; + } + } + else if (name === 'rr_mediaPlaybackRate' && + typeof value === 'number') { + node.playbackRate = value; + } + else if (name === 'rr_mediaMuted' && typeof value === 'boolean') { + node.muted = value; + } + else if (name === 'rr_mediaLoop' && typeof value === 'boolean') { + node.loop = value; + } + else if (name === 'rr_mediaVolume' && typeof value === 'number') { + node.volume = value; + } + } + if (n.isShadowHost) { + if (!node.shadowRoot) { + node.attachShadow({ mode: 'open' }); + } + else { + while (node.shadowRoot.firstChild) { + node.shadowRoot.removeChild(node.shadowRoot.firstChild); + } + } + } + return node; + } + case NodeType$2.Text: + return doc.createTextNode(n.isStyle && hackCss + ? adaptCssForReplay(n.textContent, cache) + : n.textContent); + case NodeType$2.CDATA: + return doc.createCDATASection(n.textContent); + case NodeType$2.Comment: + return doc.createComment(n.textContent); + default: + return null; + } +} +function buildNodeWithSN(n, options) { + const { doc, mirror, skipChild = false, hackCss = true, afterAppend, cache, } = options; + if (mirror.has(n.id)) { + const nodeInMirror = mirror.getNode(n.id); + const meta = mirror.getMeta(nodeInMirror); + if (isNodeMetaEqual(meta, n)) + return mirror.getNode(n.id); + } + let node = buildNode(n, { doc, hackCss, cache }); + if (!node) { + return null; + } + if (n.rootId && mirror.getNode(n.rootId) !== doc) { + mirror.replace(n.rootId, doc); + } + if (n.type === NodeType$2.Document) { + doc.close(); + doc.open(); + if (n.compatMode === 'BackCompat' && + n.childNodes && + n.childNodes[0].type !== NodeType$2.DocumentType) { + if (n.childNodes[0].type === NodeType$2.Element && + 'xmlns' in n.childNodes[0].attributes && + n.childNodes[0].attributes.xmlns === 'http://www.w3.org/1999/xhtml') { + doc.write(''); + } + else { + doc.write(''); + } + } + node = doc; + } + mirror.add(node, n); + if ((n.type === NodeType$2.Document || n.type === NodeType$2.Element) && + !skipChild) { + for (const childN of n.childNodes) { + const childNode = buildNodeWithSN(childN, { + doc, + mirror, + skipChild: false, + hackCss, + afterAppend, + cache, + }); + if (!childNode) { + console.warn('Failed to rebuild', childN); + continue; + } + if (childN.isShadow && isElement(node) && node.shadowRoot) { + node.shadowRoot.appendChild(childNode); + } + else if (n.type === NodeType$2.Document && + childN.type == NodeType$2.Element) { + const htmlElement = childNode; + let body = null; + htmlElement.childNodes.forEach((child) => { + if (child.nodeName === 'BODY') + body = child; + }); + if (body) { + htmlElement.removeChild(body); + node.appendChild(childNode); + htmlElement.appendChild(body); + } + else { + node.appendChild(childNode); + } + } + else { + node.appendChild(childNode); + } + if (afterAppend) { + afterAppend(childNode, childN.id); + } + } + } + return node; +} +function visit(mirror, onVisit) { + function walk(node) { + onVisit(node); + } + for (const id of mirror.getIds()) { + if (mirror.has(id)) { + walk(mirror.getNode(id)); + } + } +} +function handleScroll(node, mirror) { + const n = mirror.getMeta(node); + if ((n === null || n === void 0 ? void 0 : n.type) !== NodeType$2.Element) { + return; + } + const el = node; + for (const name in n.attributes) { + if (!(Object.prototype.hasOwnProperty.call(n.attributes, name) && + name.startsWith('rr_'))) { + continue; + } + const value = n.attributes[name]; + if (name === 'rr_scrollLeft') { + el.scrollLeft = value; + } + if (name === 'rr_scrollTop') { + el.scrollTop = value; + } + } +} +function rebuild(n, options) { + const { doc, onVisit, hackCss = true, afterAppend, cache, mirror = new Mirror$2(), } = options; + const node = buildNodeWithSN(n, { + doc, + mirror, + skipChild: false, + hackCss, + afterAppend, + cache, + }); + visit(mirror, (visitedNode) => { + if (onVisit) { + onVisit(visitedNode); + } + handleScroll(visitedNode, mirror); + }); + return node; +} + +function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); +} +const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; +exports.mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, +}; +if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + exports.mirror = new Proxy(exports.mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); +} +function throttle(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (...args) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; +} +function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); +} +function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { + }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { + }; + } +} +let nowTimestamp = Date.now; +if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) { + nowTimestamp = () => new Date().getTime(); +} +function getWindowScroll(win) { + var _a, _b, _c, _d, _e, _f; + const doc = win.document; + return { + left: doc.scrollingElement + ? doc.scrollingElement.scrollLeft + : win.pageXOffset !== undefined + ? win.pageXOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollLeft) || + ((_b = (_a = doc === null || doc === void 0 ? void 0 : doc.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || + ((_c = doc === null || doc === void 0 ? void 0 : doc.body) === null || _c === void 0 ? void 0 : _c.scrollLeft) || + 0, + top: doc.scrollingElement + ? doc.scrollingElement.scrollTop + : win.pageYOffset !== undefined + ? win.pageYOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollTop) || + ((_e = (_d = doc === null || doc === void 0 ? void 0 : doc.body) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.scrollTop) || + ((_f = doc === null || doc === void 0 ? void 0 : doc.body) === null || _f === void 0 ? void 0 : _f.scrollTop) || + 0, + }; +} +function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); +} +function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); +} +function closestElementOfNode(node) { + if (!node) { + return null; + } + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + return el; +} +function isBlocked(node, blockClass, blockSelector, checkAncestors) { + if (!node) { + return false; + } + const el = closestElementOfNode(node); + if (!el) { + return false; + } + try { + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) + return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) + return true; + } + else { + if (classMatchesRegex(el, blockClass, checkAncestors)) + return true; + } + } + catch (e) { + } + if (blockSelector) { + if (el.matches(blockSelector)) + return true; + if (checkAncestors && el.closest(blockSelector) !== null) + return true; + } + return false; +} +function isSerialized(n, mirror) { + return mirror.getId(n) !== -1; +} +function isIgnored(n, mirror) { + return mirror.getId(n) === IGNORED_NODE; +} +function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); +} +function legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); +} +function polyfill$1(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = (...args) => { + let node = args[0]; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } +} +function queueToResolveTrees(queue) { + const queueNodeMap = {}; + const putIntoMap = (m, parent) => { + const nodeInTree = { + value: m, + parent, + children: [], + }; + queueNodeMap[m.node.id] = nodeInTree; + return nodeInTree; + }; + const queueNodeTrees = []; + for (const mutation of queue) { + const { nextId, parentId } = mutation; + if (nextId && nextId in queueNodeMap) { + const nextInTree = queueNodeMap[nextId]; + if (nextInTree.parent) { + const idx = nextInTree.parent.children.indexOf(nextInTree); + nextInTree.parent.children.splice(idx, 0, putIntoMap(mutation, nextInTree.parent)); + } + else { + const idx = queueNodeTrees.indexOf(nextInTree); + queueNodeTrees.splice(idx, 0, putIntoMap(mutation, null)); + } + continue; + } + if (parentId in queueNodeMap) { + const parentInTree = queueNodeMap[parentId]; + parentInTree.children.push(putIntoMap(mutation, parentInTree)); + continue; + } + queueNodeTrees.push(putIntoMap(mutation, null)); + } + return queueNodeTrees; +} +function iterateResolveTree(tree, cb) { + cb(tree.value); + for (let i = tree.children.length - 1; i >= 0; i--) { + iterateResolveTree(tree.children[i], cb); + } +} +function isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); +} +function isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); +} +function getBaseDimension(node, rootIframe) { + var _a, _b; + const frameElement = (_b = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView) === null || _b === void 0 ? void 0 : _b.frameElement; + if (!frameElement || frameElement === rootIframe) { + return { + x: 0, + y: 0, + relativeScale: 1, + absoluteScale: 1, + }; + } + const frameDimension = frameElement.getBoundingClientRect(); + const frameBaseDimension = getBaseDimension(frameElement, rootIframe); + const relativeScale = frameDimension.height / frameElement.clientHeight; + return { + x: frameDimension.x * frameBaseDimension.relativeScale + + frameBaseDimension.x, + y: frameDimension.y * frameBaseDimension.relativeScale + + frameBaseDimension.y, + relativeScale, + absoluteScale: frameBaseDimension.absoluteScale * relativeScale, + }; +} +function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); +} +function getNestedRule(rules, position) { + const rule = rules[position[0]]; + if (position.length === 1) { + return rule; + } + else { + return getNestedRule(rule.cssRules[position[1]].cssRules, position.slice(2)); + } +} +function getPositionsAndIndex(nestedIndex) { + const positions = [...nestedIndex]; + const index = positions.pop(); + return { positions, index }; +} +function uniqueTextMutations(mutations) { + const idSet = new Set(); + const uniqueMutations = []; + for (let i = mutations.length; i--;) { + const mutation = mutations[i]; + if (!idSet.has(mutation.id)) { + uniqueMutations.push(mutation); + idSet.add(mutation.id); + } + } + return uniqueMutations; +} +class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + } + getId(stylesheet) { + var _a; + return (_a = this.styleIDMap.get(stylesheet)) !== null && _a !== void 0 ? _a : -1; + } + has(stylesheet) { + return this.styleIDMap.has(stylesheet); + } + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; + } + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + getStyle(id) { + return this.idStyleMap.get(id) || null; + } + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; + } + generateId() { + return this.id++; + } +} +function getShadowHost(n) { + var _a, _b; + let shadowHost = null; + if (((_b = (_a = n.getRootNode) === null || _a === void 0 ? void 0 : _a.call(n)) === null || _b === void 0 ? void 0 : _b.nodeType) === Node.DOCUMENT_FRAGMENT_NODE && + n.getRootNode().host) + shadowHost = n.getRootNode().host; + return shadowHost; +} +function getRootShadowHost(n) { + let rootShadowHost = n; + let shadowHost; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; + return rootShadowHost; +} +function shadowHostInDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + const shadowHost = getRootShadowHost(n); + return doc.contains(shadowHost); +} +function inDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + return doc.contains(n) || shadowHostInDom(n); +} + +var utils = /*#__PURE__*/Object.freeze({ + __proto__: null, + on: on, + get _mirror () { return exports.mirror; }, + throttle: throttle, + hookSetter: hookSetter, + patch: patch, + get nowTimestamp () { return nowTimestamp; }, + getWindowScroll: getWindowScroll, + getWindowHeight: getWindowHeight, + getWindowWidth: getWindowWidth, + closestElementOfNode: closestElementOfNode, + isBlocked: isBlocked, + isSerialized: isSerialized, + isIgnored: isIgnored, + isAncestorRemoved: isAncestorRemoved, + legacy_isTouchEvent: legacy_isTouchEvent, + polyfill: polyfill$1, + queueToResolveTrees: queueToResolveTrees, + iterateResolveTree: iterateResolveTree, + isSerializedIframe: isSerializedIframe, + isSerializedStylesheet: isSerializedStylesheet, + getBaseDimension: getBaseDimension, + hasShadowRoot: hasShadowRoot, + getNestedRule: getNestedRule, + getPositionsAndIndex: getPositionsAndIndex, + uniqueTextMutations: uniqueTextMutations, + StyleSheetMirror: StyleSheetMirror, + getShadowHost: getShadowHost, + getRootShadowHost: getRootShadowHost, + shadowHostInDom: shadowHostInDom, + inDom: inDom +}); + +var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; +})(EventType || {}); +var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; +})(IncrementalSource || {}); +var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; +})(MouseInteractions || {}); +var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; +})(PointerTypes || {}); +var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; +})(CanvasContext || {}); +var ReplayerEvents = /* @__PURE__ */ ((ReplayerEvents2) => { + ReplayerEvents2["Start"] = "start"; + ReplayerEvents2["Pause"] = "pause"; + ReplayerEvents2["Resume"] = "resume"; + ReplayerEvents2["Resize"] = "resize"; + ReplayerEvents2["Finish"] = "finish"; + ReplayerEvents2["FullsnapshotRebuilded"] = "fullsnapshot-rebuilded"; + ReplayerEvents2["LoadStylesheetStart"] = "load-stylesheet-start"; + ReplayerEvents2["LoadStylesheetEnd"] = "load-stylesheet-end"; + ReplayerEvents2["SkipStart"] = "skip-start"; + ReplayerEvents2["SkipEnd"] = "skip-end"; + ReplayerEvents2["MouseInteraction"] = "mouse-interaction"; + ReplayerEvents2["EventCast"] = "event-cast"; + ReplayerEvents2["CustomEvent"] = "custom-event"; + ReplayerEvents2["Flush"] = "flush"; + ReplayerEvents2["StateChange"] = "state-change"; + ReplayerEvents2["PlayBack"] = "play-back"; + ReplayerEvents2["Destroy"] = "destroy"; + return ReplayerEvents2; +})(ReplayerEvents || {}); + +function isNodeInLinkedList(n) { + return '__ln' in n; +} +class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + this.tail = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + if (node.next === null) { + this.tail = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + else { + this.tail = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } +} +const moveKey = (id, parentId) => `${id}@${parentId}`; +class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addedIds = new Set(); + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + if (!n.parentNode || + !inDom(n) || + n.parentNode.tagName === 'TEXTAREA') { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + let tailNode = addList.tail; + while (tailNode) { + const _node = tailNode; + tailNode = tailNode.previous; + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (nextId === -1) + continue; + else if (parentId !== -1) { + node = _node; + break; + } + else { + const unhandledNode = _node.value; + if (unhandledNode.parentNode && + unhandledNode.parentNode.nodeType === + Node.DOCUMENT_FRAGMENT_NODE) { + const shadowHost = unhandledNode.parentNode + .host; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => { + const n = text.node; + if (n.parentNode && + n.parentNode.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(n.parentNode); + } + return { + id: this.mirror.getId(n), + value: text.value, + }; + }) + .filter((text) => !addedIds.has(text.id)) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => { + const { attributes } = attribute; + if (typeof attributes.style === 'string') { + const diffAsStr = JSON.stringify(attribute.styleDiff); + const unchangedAsStr = JSON.stringify(attribute._unchangedStyles); + if (diffAsStr.length < attributes.style.length) { + if ((diffAsStr + unchangedAsStr).split('var(').length === + attributes.style.split('var(').length) { + attributes.style = attribute.styleDiff; + } + } + } + return { + id: this.mirror.getId(attribute.node), + attributes: attributes, + }; + }) + .filter((attribute) => !addedIds.has(attribute.id)) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.genTextAreaValueMutation = (textarea) => { + let item = this.attributeMap.get(textarea); + if (!item) { + item = { + node: textarea, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(textarea, item); + } + item.attributes.value = Array.from(textarea.childNodes, (cn) => cn.textContent || '').join(''); + }; + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, false) && + value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, true) && value + ? this.maskTextFn + ? this.maskTextFn(value, closestElementOfNode(m.target)) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let attributeName = m.attributeName; + let value = m.target.getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ + element: target, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type, + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + value === m.oldValue) { + return; + } + let item = this.attributeMap.get(m.target); + if (target.tagName === 'IFRAME' && + attributeName === 'src' && + !this.keepIframeSrcFn(value)) { + if (!target.contentDocument) { + attributeName = 'rr_src'; + } + else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(m.target, item); + } + if (attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (!ignoreAttribute(target.tagName, attributeName)) { + item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value); + if (attributeName === 'style') { + if (!this.unattachedDoc) { + try { + this.unattachedDoc = + document.implementation.createHTMLDocument(); + } + catch (e) { + this.unattachedDoc = this.doc; + } + } + const old = this.unattachedDoc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + item.styleDiff[pname] = newValue; + } + else { + item.styleDiff[pname] = [newValue, newPriority]; + } + } + else { + item._unchangedStyles[pname] = [newValue, newPriority]; + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + item.styleDiff[pname] = false; + } + } + } + } + break; + } + case 'childList': { + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; + if (m.target.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(m.target); + return; + } + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (this.processedNodeManager.inOtherBuffer(n, this)) + return; + if (this.addedSet.has(n) || this.movedSet.has(n)) + return; + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); + } + } + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'dataURLOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'processedNodeManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } +} +function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); +} +function isParentRemoved(removes, n, mirror) { + if (removes.length === 0) + return false; + return _isParentRemoved(removes, n, mirror); +} +function _isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); +} +function isAncestorInSet(set, n) { + if (set.size === 0) + return false; + return _isAncestorInSet(set, n); +} +function _isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); +} + +let errorHandler; +function registerErrorHandler(handler) { + errorHandler = handler; +} +function unregisterErrorHandler() { + errorHandler = undefined; +} +const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { + try { + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return; + } + throw error; + } + }); + return rrwebWrapped; +}; + +const mutationBuffers = []; +function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { + } + return event && event.target; +} +function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer))); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; +} +function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { + }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle(callbackWrapper((source) => { + const totalOffset = Date.now() - timeBaseline; + mousemoveCb(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }), callbackThreshold); + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = legacy_isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = nowTimestamp(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: nowTimestamp() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource.Drag + : evt instanceof MouseEvent + ? IncrementalSource.MouseMove + : IncrementalSource.TouchMove); + }), threshold, { + trailing: false, + })); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { + }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + let currentPointerType = null; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, true)) { + return; + } + let pointerType = null; + let thisEventKey = eventKey; + if ('pointerType' in event) { + switch (event.pointerType) { + case 'mouse': + pointerType = PointerTypes.Mouse; + break; + case 'touch': + pointerType = PointerTypes.Touch; + break; + case 'pen': + pointerType = PointerTypes.Pen; + break; + } + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + thisEventKey = 'TouchStart'; + } + else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) { + thisEventKey = 'TouchEnd'; + } + } + else if (pointerType === PointerTypes.Pen) ; + } + else if (legacy_isTouchEvent(event)) { + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + if ((thisEventKey.startsWith('Touch') && + pointerType === PointerTypes.Touch) || + (thisEventKey.startsWith('Mouse') && + pointerType === PointerTypes.Mouse)) { + pointerType = null; + } + } + else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; + } + const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)(Object.assign({ type: MouseInteractions[thisEventKey], id, x: clientX, y: clientY }, (pointerType !== null && { pointerType }))); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + let eventName = toLowerCase(eventKey); + const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + return; + } + } + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }) { + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); + } + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }), sampling.scroll || 100)); + return on('scroll', updatePosition, doc); +} +function initViewportResizeObserver({ viewportResizeCb }, { win }) { + let lastH = -1; + let lastW = -1; + const updateDimension = callbackWrapper(throttle(callbackWrapper(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }), 200)); + return on('resize', updateDimension, win); +} +const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; +const lastInputValueMap = new WeakMap(); +function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + if (target.classList.contains(ignoreClass) || + (ignoreSelector && target.matches(ignoreSelector))) { + return; + } + let text = target.value; + let isChecked = false; + const type = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + else if (maskInputOptions[tagName.toLowerCase()] || + maskInputOptions[type]) { + text = maskInputValue({ + element: target, + maskInputOptions, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, userTriggeredOnInput + ? { text, isChecked, userTriggered } + : { text, isChecked }); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = el.value; + cbWithDedup(el, userTriggeredOnInput + ? { text, isChecked: !isChecked, userTriggered: false } + : { text, isChecked: !isChecked }); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + callbackWrapper(inputCb)(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const currentWindow = doc.defaultView; + if (!currentWindow) { + return () => { + handlers.forEach((h) => h()); + }; + } + const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [currentWindow.HTMLInputElement.prototype, 'value'], + [currentWindow.HTMLInputElement.prototype, 'checked'], + [currentWindow.HTMLSelectElement.prototype, 'value'], + [currentWindow.HTMLTextAreaElement.prototype, 'value'], + [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'], + [currentWindow.HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ + target: this, + isTrusted: false, + }); + }, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); +} +function getIdAndStyleId(sheet, mirror, styleMirror) { + let id, styleId; + if (!sheet) + return {}; + if (sheet.ownerNode) + id = mirror.getId(sheet.ownerNode); + else + styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; +} +function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { + }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + let replace; + if (win.CSSStyleSheet.prototype.replace) { + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = new Proxy(replace, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + let replaceSync; + if (win.CSSStyleSheet.prototype.replaceSync) { + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); +} +function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) { + var _a, _b, _c; + let hostId = null; + if (host.nodeName === '#document') + hostId = mirror.getId(host); + else + hostId = mirror.getId(host.host); + const patchTarget = host.nodeName === '#document' + ? (_a = host.defaultView) === null || _a === void 0 ? void 0 : _a.Document + : (_c = (_b = host.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.ShadowRoot; + const originalPropertyDescriptor = (patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype) + ? Object.getOwnPropertyDescriptor(patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype, 'adoptedStyleSheets') + : undefined; + if (hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor) + return () => { + }; + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get() { + var _a; + return (_a = originalPropertyDescriptor.get) === null || _a === void 0 ? void 0 : _a.call(this); + }, + set(sheets) { + var _a; + const result = (_a = originalPropertyDescriptor.set) === null || _a === void 0 ? void 0 : _a.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } + catch (e) { + } + } + return result; + }, + }); + return callbackWrapper(() => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get: originalPropertyDescriptor.get, + set: originalPropertyDescriptor.set, + }); + }); +} +function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property, value, priority] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return setProperty.apply(thisArg, [property, value, priority]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return removeProperty.apply(thisArg, [property]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); +} +function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, mirror, sampling, doc, }) { + const handler = callbackWrapper((type) => throttle(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const { currentTime, volume, muted, playbackRate, loop } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + playbackRate, + loop, + }); + }), sampling.media || 500)); + const handlers = [ + on('play', handler(0), doc), + on('pause', handler(1), doc), + on('seeked', handler(2), doc), + on('volumechange', handler(3), doc), + on('ratechange', handler(4), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { + }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(callbackWrapper(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }), 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); +} +function initSelectionObserver(param) { + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; + let collapsed = true; + const updateSelection = callbackWrapper(() => { + const selection = doc.getSelection(); + if (!selection || (collapsed && (selection === null || selection === void 0 ? void 0 : selection.isCollapsed))) + return; + collapsed = selection.isCollapsed || false; + const ranges = []; + const count = selection.rangeCount || 0; + for (let i = 0; i < count; i++) { + const range = selection.getRangeAt(i); + const { startContainer, startOffset, endContainer, endOffset } = range; + const blocked = isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); + if (blocked) + continue; + ranges.push({ + start: mirror.getId(startContainer), + startOffset, + end: mirror.getId(endContainer), + endOffset, + }); + } + selectionCb({ ranges }); + }); + updateSelection(); + return on('selectionchange', updateSelection); +} +function initCustomElementObserver({ doc, customElementCb, }) { + const win = doc.defaultView; + if (!win || !win.customElements) + return () => { }; + const restoreHandler = patch(win.customElements, 'define', function (original) { + return function (name, constructor, options) { + try { + customElementCb({ + define: { + name, + }, + }); + } + catch (e) { + console.warn(`Custom element callback failed for ${name}`); + } + return original.apply(this, [name, constructor, options]); + }; + }); + return restoreHandler; +} +function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, selectionCb, customElementCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; + o.selectionCb = (...p) => { + if (hooks.selection) { + hooks.selection(...p); + } + selectionCb(...p); + }; + o.customElementCb = (...c) => { + if (hooks.customElement) { + hooks.customElement(...c); + } + customElementCb(...c); + }; +} +function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { + }; + } + mergeHooks(o, hooks); + let mutationObserver; + if (o.recordDOM) { + mutationObserver = initMutationObserver(o, o.doc); + } + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o, { + win: currentWindow, + }); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + let styleSheetObserver = () => { }; + let adoptedStyleSheetObserver = () => { }; + let styleDeclarationObserver = () => { }; + let fontObserver = () => { }; + if (o.recordDOM) { + styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); + styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + if (o.collectFonts) { + fontObserver = initFontObserver(o); + } + } + const selectionObserver = initSelectionObserver(o); + const customElementObserver = initCustomElementObserver(o); + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver === null || mutationObserver === void 0 ? void 0 : mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + adoptedStyleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + selectionObserver(); + customElementObserver(); + pluginHandlers.forEach((h) => h()); + }); +} +function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; +} +function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); +} + +class CrossOriginIframeMirror { + constructor(generateIdFn) { + this.generateIdFn = generateIdFn; + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + } + getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) { + const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe); + let id = idToRemoteIdMap.get(remoteId); + if (!id) { + id = this.generateIdFn(); + idToRemoteIdMap.set(remoteId, id); + remoteIdToIdMap.set(id, remoteId); + } + return id; + } + getIds(iframe, remoteId) { + const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + } + getRemoteId(iframe, id, map) { + const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe); + if (typeof id !== 'number') + return id; + const remoteId = remoteIdToIdMap.get(id); + if (!remoteId) + return -1; + return remoteId; + } + getRemoteIds(iframe, ids) { + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + } + reset(iframe) { + if (!iframe) { + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + return; + } + this.iframeIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToIdMap.delete(iframe); + } + getIdToRemoteIdMap(iframe) { + let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); + if (!idToRemoteIdMap) { + idToRemoteIdMap = new Map(); + this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + } + return idToRemoteIdMap; + } + getRemoteIdToIdMap(iframe) { + let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); + if (!remoteIdToIdMap) { + remoteIdToIdMap = new Map(); + this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + } + return remoteIdToIdMap; + } +} + +class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.crossOriginIframeMap = new WeakMap(); + this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + this.crossOriginIframeRootIdMap = new WeakMap(); + this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; + this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)); + this.mirror = options.mirror; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + if (iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0) + this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument)); + } + handleMessage(message) { + const crossOriginMessageEvent = message; + if (crossOriginMessageEvent.data.type !== 'rrweb' || + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin) + return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) + return; + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) + return; + const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event); + if (transformedEvent) + this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout); + } + transformCrossOriginEvent(iframeEl, e) { + var _a; + switch (e.type) { + case EventType.FullSnapshot: { + this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); + this.replaceIdOnNode(e.data.node, iframeEl); + const rootId = e.data.node.id; + this.crossOriginIframeRootIdMap.set(iframeEl, rootId); + this.patchRootIdOnNode(e.data.node, rootId); + return { + timestamp: e.timestamp, + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + case EventType.Meta: + case EventType.Load: + case EventType.DomContentLoaded: { + return false; + } + case EventType.Plugin: { + return e; + } + case EventType.Custom: { + this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); + return e; + } + case EventType.IncrementalSnapshot: { + switch (e.data.source) { + case IncrementalSource.Mutation: { + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, [ + 'parentId', + 'nextId', + 'previousId', + ]); + this.replaceIdOnNode(n.node, iframeEl); + const rootId = this.crossOriginIframeRootIdMap.get(iframeEl); + rootId && this.patchRootIdOnNode(n.node, rootId); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource.Drag: + case IncrementalSource.TouchMove: + case IncrementalSource.MouseMove: { + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource.ViewportResize: { + return false; + } + case IncrementalSource.MediaInteraction: + case IncrementalSource.MouseInteraction: + case IncrementalSource.Scroll: + case IncrementalSource.CanvasMutation: + case IncrementalSource.Input: { + this.replaceIds(e.data, iframeEl, ['id']); + return e; + } + case IncrementalSource.StyleSheetRule: + case IncrementalSource.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleId']); + return e; + } + case IncrementalSource.Font: { + return e; + } + case IncrementalSource.Selection: { + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); + return e; + } + case IncrementalSource.AdoptedStyleSheet: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); + return e; + } + } + } + } + return false; + } + replace(iframeMirror, obj, iframeEl, keys) { + for (const key of keys) { + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') + continue; + if (Array.isArray(obj[key])) { + obj[key] = iframeMirror.getIds(iframeEl, obj[key]); + } + else { + obj[key] = iframeMirror.getId(iframeEl, obj[key]); + } + } + return obj; + } + replaceIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); + } + replaceStyleIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); + } + replaceIdOnNode(node, iframeEl) { + this.replaceIds(node, iframeEl, ['id', 'rootId']); + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } + patchRootIdOnNode(node, rootId) { + if (node.type !== NodeType$2.Document && !node.rootId) + node.rootId = rootId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.patchRootIdOnNode(child, rootId); + }); + } + } +} + +class ShadowDomManager { + constructor(options) { + this.shadowDoms = new WeakSet(); + this.restoreHandlers = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + this.init(); + } + init() { + this.reset(); + this.patchAttachShadow(Element, document); + } + addShadowRoot(shadowRoot, doc) { + if (!isNativeShadowDom(shadowRoot)) + return; + if (this.shadowDoms.has(shadowRoot)) + return; + this.shadowDoms.add(shadowRoot); + const observer = initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + this.restoreHandlers.push(() => observer.disconnect()); + this.restoreHandlers.push(initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror }))); + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0) + this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host)); + this.restoreHandlers.push(initAdoptedStyleSheetObserver({ + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, shadowRoot)); + }, 0); + } + observeAttachShadow(iframeElement) { + if (!iframeElement.contentWindow || !iframeElement.contentDocument) + return; + this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument); + } + patchAttachShadow(element, doc) { + const manager = this; + this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) { + return function (option) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot && inDom(this)) + manager.addShadowRoot(this.shadowRoot, doc); + return shadowRoot; + }; + })); + } + reset() { + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } + catch (e) { + } + }); + this.restoreHandlers = []; + this.shadowDoms = new WeakSet(); + } +} + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +} + +function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +/* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ +var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +// Use a lookup table to find the index. +var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); +for (var i$2 = 0; i$2 < chars.length; i$2++) { + lookup[chars.charCodeAt(i$2)] = i$2; +} +var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; +}; +var decode = function (base64) { + var bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4; + if (base64[base64.length - 1] === '=') { + bufferLength--; + if (base64[base64.length - 2] === '=') { + bufferLength--; + } + } + var arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer); + for (i = 0; i < len; i += 4) { + encoded1 = lookup[base64.charCodeAt(i)]; + encoded2 = lookup[base64.charCodeAt(i + 1)]; + encoded3 = lookup[base64.charCodeAt(i + 2)]; + encoded4 = lookup[base64.charCodeAt(i + 3)]; + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + return arraybuffer; +}; + +const canvasVarMap = new Map(); +function variableListFor$1(ctx, ctor) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); +} +const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor$1(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; +}; +function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + const src = value.toDataURL(); + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; +} +const serializeArgs = (args, win, ctx) => { + return args.map((arg) => serializeArg(arg, win, ctx)); +}; +const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); +}; + +function initCanvas2DMutationObserver(cb, win, blockClass, blockSelector) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { + setTimeout(() => { + const recordArgs = serializeArgs(args, win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function getNormalizedContextName(contextType) { + return contextType === 'experimental-webgl' ? 'webgl' : contextType; +} +function initCanvasContextObserver(win, blockClass, blockSelector, setPreserveDrawingBufferToTrue) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, true)) { + const ctxName = getNormalizedContextName(contextType); + if (!('__context' in this)) + this.__context = ctxName; + if (setPreserveDrawingBufferToTrue && + ['webgl', 'webgl2'].includes(ctxName)) { + if (args[0] && typeof args[0] === 'object') { + const contextAttributes = args[0]; + if (!contextAttributes.preserveDrawingBuffer) { + contextAttributes.preserveDrawingBuffer = true; + } + } + else { + args.splice(0, 1, { + preserveDrawingBuffer: true, + }); + } + } + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function patchGLPrototype(prototype, type, cb, blockClass, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + if ([ + 'isContextLost', + 'canvas', + 'drawingBufferWidth', + 'drawingBufferHeight', + ].includes(prop)) { + continue; + } + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, this); + if ('tagName' in this.canvas && + !isBlocked(this.canvas, blockClass, blockSelector, true)) { + const recordArgs = serializeArgs(args, win, this); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; +} +function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; +} + +function funcToSource(fn, sourcemapArg) { + var sourcemap = sourcemapArg === undefined ? null : sourcemapArg; + var source = fn.toString(); + var lines = source.split('\n'); + lines.pop(); + lines.shift(); + var blankPrefixLength = lines[0].search(/\S/); + var regex = /(['"])__worker_loader_strict__(['"])/g; + for (var i = 0, n = lines.length; i < n; ++i) { + lines[i] = lines[i].substring(blankPrefixLength).replace(regex, '$1use strict$2') + '\n'; + } + if (sourcemap) { + lines.push('\/\/# sourceMappingURL=' + sourcemap + '\n'); + } + return lines; +} + +function createURL(fn, sourcemapArg) { + var lines = funcToSource(fn, sourcemapArg); + var blob = new Blob(lines, { type: 'application/javascript' }); + return URL.createObjectURL(blob); +} + +function createInlineWorkerFactory(fn, sourcemapArg) { + var url; + return function WorkerFactory(options) { + url = url || createURL(fn, sourcemapArg); + return new Worker(url, options); + }; +} + +var WorkerFactory = createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () { +(function () { + '__worker_loader_strict__'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const lastBlobMap = new Map(); + const transparentBlobMap = new Map(); + function getTransparentBlobFor(width, height, dataURLOptions) { + return __awaiter(this, void 0, void 0, function* () { + const id = `${width}-${height}`; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) + return transparentBlobMap.get(id); + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + transparentBlobMap.set(id, base64); + return base64; + } + else { + return ''; + } + }); + } + const worker = self; + worker.onmessage = function (e) { + return __awaiter(this, void 0, void 0, function* () { + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor(width, height, dataURLOptions); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const type = blob.type; + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + if (!lastBlobMap.has(id) && (yield transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + if (lastBlobMap.get(id) === base64) + return worker.postMessage({ id }); + worker.postMessage({ + id, + type, + base64, + width, + height, + }); + lastBlobMap.set(id, base64); + } + else { + return worker.postMessage({ id: e.data.id }); + } + }); + }; + +})(); +}, null); +/* eslint-enable */ + +class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = (target, mutation) => { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas, dataURLOptions, } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass, blockSelector); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { + dataURLOptions, + }); + } + initCanvasFPSObserver(fps, win, blockClass, blockSelector, options) { + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, true); + const snapshotInProgressMap = new Map(); + const worker = new WorkerFactory(); + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + if (!('base64' in e.data)) + return; + const { base64, type, width, height } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', + args: [0, 0, width, height], + }, + { + property: 'drawImage', + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + }, + 0, + 0, + ], + }, + ], + }); + }; + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId; + const getCanvas = () => { + const matchedCanvas = []; + win.document.querySelectorAll('canvas').forEach((canvas) => { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { + matchedCanvas.push(canvas); + } + }); + return matchedCanvas; + }; + const takeCanvasSnapshots = (timestamp) => { + if (lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + getCanvas() + .forEach((canvas) => __awaiter(this, void 0, void 0, function* () { + var _a; + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) + return; + if (canvas.width === 0 || canvas.height === 0) + return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes(canvas.__context)) { + const context = canvas.getContext(canvas.__context); + if (((_a = context === null || context === void 0 ? void 0 : context.getContextAttributes()) === null || _a === void 0 ? void 0 : _a.preserveDrawingBuffer) === false) { + context.clear(context.COLOR_BUFFER_BIT); + } + } + const bitmap = yield createImageBitmap(canvas); + worker.postMessage({ + id, + bitmap, + width: canvas.width, + height: canvas.height, + dataURLOptions: options.dataURLOptions, + }, [bitmap]); + })); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + rafId = requestAnimationFrame(takeCanvasSnapshots); + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + initCanvasMutationObserver(win, blockClass, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, false); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } +} + +class StylesheetManager { + constructor(options) { + this.trackedLinkElements = new WeakSet(); + this.styleMirror = new StyleSheetMirror(); + this.mutationCb = options.mutationCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; + } + attachLinkElement(linkEl, childSn) { + if ('_cssText' in childSn.attributes) + this.mutationCb({ + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: childSn.id, + attributes: childSn + .attributes, + }, + ], + }); + this.trackLinkElement(linkEl); + } + trackLinkElement(linkEl) { + if (this.trackedLinkElements.has(linkEl)) + return; + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + adoptStyleSheets(sheets, hostId) { + if (sheets.length === 0) + return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [], + }; + const styles = []; + for (const sheet of sheets) { + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + styles.push({ + styleId, + rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ + rule: stringifyRule(r), + index, + })), + }); + } + else + styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); + } + if (styles.length > 0) + adoptedStyleSheetData.styles = styles; + this.adoptedStyleSheetCb(adoptedStyleSheetData); + } + reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + trackStylesheetInLinkElement(linkEl) { + } +} + +class ProcessedNodeManager { + constructor() { + this.nodeMap = new WeakMap(); + this.loop = true; + this.periodicallyClear(); + } + periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + if (this.loop) + this.periodicallyClear(); + }); + } + inOtherBuffer(node, thisBuffer) { + const buffers = this.nodeMap.get(node); + return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + } + add(node, buffer) { + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + } + clear() { + this.nodeMap = new WeakMap(); + } + destroy() { + this.loop = false; + } +} + +function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: nowTimestamp() }); +} +let wrappedEmit; +let takeFullSnapshot; +let canvasManager; +let recording = false; +const mirror = createMirror$2(); +function record(options = {}) { + const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordDOM = true, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded' + ? options.recordAfter + : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; + registerErrorHandler(errorHandler); + const inEmittingFrame = recordCrossOriginIframes + ? window.parent === window + : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + if (window.parent.document) { + passEmitsToParent = false; + } + } + catch (e) { + passEmitsToParent = true; + } + } + if (inEmittingFrame && !emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + mirror.reset(); + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill$1(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn && + !passEmitsToParent) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType.FullSnapshot && + !(e.type === EventType.IncrementalSnapshot && + e.data.source === IncrementalSource.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + if (inEmittingFrame) { + emit === null || emit === void 0 ? void 0 : emit(eventProcessor(e), isCheckout); + } + else if (passEmitsToParent) { + const message = { + type: 'rrweb', + event: eventProcessor(e), + origin: window.location.origin, + isCheckout, + }; + window.parent.postMessage(message, '*'); + } + if (e.type === EventType.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType.IncrementalSnapshot) { + if (e.data.source === IncrementalSource.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.CanvasMutation }, p), + })); + const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.AdoptedStyleSheet }, a), + })); + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, + }); + const iframeManager = new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + }); + } + const processedNodeManager = new ProcessedNodeManager(); + canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + if (!recordDOM) { + return; + } + wrappedEmit(wrapEvent({ + type: EventType.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + stylesheetManager.reset(); + shadowDomManager.init(); + mutationBuffers.forEach((buf) => buf.lock()); + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + dataURLOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.trackLinkElement(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachLinkElement(linkEl, childSn); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + wrappedEmit(wrapEvent({ + type: EventType.FullSnapshot, + data: { + node, + initialOffset: getWindowScroll(window), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.unlock()); + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document)); + }; + try { + const handlers = []; + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Font }, p), + })), + selectionCb: (p) => { + wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.Selection }, p), + })); + }, + customElementCb: (c) => { + wrappedEmit(wrapEvent({ + type: EventType.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource.CustomElement }, c), + })); + }, + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordDOM, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + dataURLOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + processedNodeManager, + canvasManager, + ignoreCSSAttributes, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + recording = true; + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType.DomContentLoaded, + data: {}, + })); + if (recordAfter === 'DOMContentLoaded') + init(); + })); + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType.Load, + data: {}, + })); + if (recordAfter === 'load') + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + processedNodeManager.destroy(); + recording = false; + unregisterErrorHandler(); + }; + } + catch (error) { + console.warn(error); + } +} +record.addCustomEvent = (tag, payload) => { + if (!recording) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType.Custom, + data: { + tag, + payload, + }, + })); +}; +record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); +}; +record.takeFullSnapshot = (isCheckout) => { + if (!recording) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); +}; +record.mirror = mirror; + +var NodeType$1; +(function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; +})(NodeType$1 || (NodeType$1 = {})); +class Mirror$1 { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} +function createMirror$1() { + return new Mirror$1(); +} + +function parseCSSText(cssText) { + const res = {}; + const listDelimiter = /;(?![^(]*\))/g; + const propertyDelimiter = /:(.+)/; + const comment = /\/\*.*?\*\//g; + cssText + .replace(comment, '') + .split(listDelimiter) + .forEach(function (item) { + if (item) { + const tmp = item.split(propertyDelimiter); + tmp.length > 1 && (res[camelize(tmp[0].trim())] = tmp[1].trim()); + } + }); + return res; +} +function toCSSText(style) { + const properties = []; + for (const name in style) { + const value = style[name]; + if (typeof value !== 'string') + continue; + const normalizedName = hyphenate(name); + properties.push(`${normalizedName}: ${value};`); + } + return properties.join(' '); +} +const camelizeRE = /-([a-z])/g; +const CUSTOM_PROPERTY_REGEX = /^--[a-zA-Z0-9-]+$/; +const camelize = (str) => { + if (CUSTOM_PROPERTY_REGEX.test(str)) + return str; + return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); +}; +const hyphenateRE = /\B([A-Z])/g; +const hyphenate = (str) => { + return str.replace(hyphenateRE, '-$1').toLowerCase(); +}; + +class BaseRRNode { + constructor(..._args) { + this.parentElement = null; + this.parentNode = null; + this.firstChild = null; + this.lastChild = null; + this.previousSibling = null; + this.nextSibling = null; + this.ELEMENT_NODE = NodeType.ELEMENT_NODE; + this.TEXT_NODE = NodeType.TEXT_NODE; + } + get childNodes() { + const childNodes = []; + let childIterator = this.firstChild; + while (childIterator) { + childNodes.push(childIterator); + childIterator = childIterator.nextSibling; + } + return childNodes; + } + contains(node) { + if (!(node instanceof BaseRRNode)) + return false; + else if (node.ownerDocument !== this.ownerDocument) + return false; + else if (node === this) + return true; + while (node.parentNode) { + if (node.parentNode === this) + return true; + node = node.parentNode; + } + return false; + } + appendChild(_newChild) { + throw new Error(`RRDomException: Failed to execute 'appendChild' on 'RRNode': This RRNode type does not support this method.`); + } + insertBefore(_newChild, _refChild) { + throw new Error(`RRDomException: Failed to execute 'insertBefore' on 'RRNode': This RRNode type does not support this method.`); + } + removeChild(_node) { + throw new Error(`RRDomException: Failed to execute 'removeChild' on 'RRNode': This RRNode type does not support this method.`); + } + toString() { + return 'RRNode'; + } +} +function BaseRRDocumentImpl(RRNodeClass) { + return class BaseRRDocument extends RRNodeClass { + constructor(...args) { + super(args); + this.nodeType = NodeType.DOCUMENT_NODE; + this.nodeName = '#document'; + this.compatMode = 'CSS1Compat'; + this.RRNodeType = NodeType$1.Document; + this.textContent = null; + this.ownerDocument = this; + } + get documentElement() { + return (this.childNodes.find((node) => node.RRNodeType === NodeType$1.Element && + node.tagName === 'HTML') || null); + } + get body() { + var _a; + return (((_a = this.documentElement) === null || _a === void 0 ? void 0 : _a.childNodes.find((node) => node.RRNodeType === NodeType$1.Element && + node.tagName === 'BODY')) || null); + } + get head() { + var _a; + return (((_a = this.documentElement) === null || _a === void 0 ? void 0 : _a.childNodes.find((node) => node.RRNodeType === NodeType$1.Element && + node.tagName === 'HEAD')) || null); + } + get implementation() { + return this; + } + get firstElementChild() { + return this.documentElement; + } + appendChild(newChild) { + const nodeType = newChild.RRNodeType; + if (nodeType === NodeType$1.Element || + nodeType === NodeType$1.DocumentType) { + if (this.childNodes.some((s) => s.RRNodeType === nodeType)) { + throw new Error(`RRDomException: Failed to execute 'appendChild' on 'RRNode': Only one ${nodeType === NodeType$1.Element ? 'RRElement' : 'RRDoctype'} on RRDocument allowed.`); + } + } + const child = appendChild(this, newChild); + child.parentElement = null; + return child; + } + insertBefore(newChild, refChild) { + const nodeType = newChild.RRNodeType; + if (nodeType === NodeType$1.Element || + nodeType === NodeType$1.DocumentType) { + if (this.childNodes.some((s) => s.RRNodeType === nodeType)) { + throw new Error(`RRDomException: Failed to execute 'insertBefore' on 'RRNode': Only one ${nodeType === NodeType$1.Element ? 'RRElement' : 'RRDoctype'} on RRDocument allowed.`); + } + } + const child = insertBefore(this, newChild, refChild); + child.parentElement = null; + return child; + } + removeChild(node) { + return removeChild(this, node); + } + open() { + this.firstChild = null; + this.lastChild = null; + } + close() { + } + write(content) { + let publicId; + if (content === + '') + publicId = '-//W3C//DTD XHTML 1.0 Transitional//EN'; + else if (content === + '') + publicId = '-//W3C//DTD HTML 4.0 Transitional//EN'; + if (publicId) { + const doctype = this.createDocumentType('html', publicId, ''); + this.open(); + this.appendChild(doctype); + } + } + createDocument(_namespace, _qualifiedName, _doctype) { + return new BaseRRDocument(); + } + createDocumentType(qualifiedName, publicId, systemId) { + const doctype = new (BaseRRDocumentTypeImpl(BaseRRNode))(qualifiedName, publicId, systemId); + doctype.ownerDocument = this; + return doctype; + } + createElement(tagName) { + const element = new (BaseRRElementImpl(BaseRRNode))(tagName); + element.ownerDocument = this; + return element; + } + createElementNS(_namespaceURI, qualifiedName) { + return this.createElement(qualifiedName); + } + createTextNode(data) { + const text = new (BaseRRTextImpl(BaseRRNode))(data); + text.ownerDocument = this; + return text; + } + createComment(data) { + const comment = new (BaseRRCommentImpl(BaseRRNode))(data); + comment.ownerDocument = this; + return comment; + } + createCDATASection(data) { + const CDATASection = new (BaseRRCDATASectionImpl(BaseRRNode))(data); + CDATASection.ownerDocument = this; + return CDATASection; + } + toString() { + return 'RRDocument'; + } + }; +} +function BaseRRDocumentTypeImpl(RRNodeClass) { + return class BaseRRDocumentType extends RRNodeClass { + constructor(qualifiedName, publicId, systemId) { + super(); + this.nodeType = NodeType.DOCUMENT_TYPE_NODE; + this.RRNodeType = NodeType$1.DocumentType; + this.name = qualifiedName; + this.publicId = publicId; + this.systemId = systemId; + this.nodeName = qualifiedName; + this.textContent = null; + } + toString() { + return 'RRDocumentType'; + } + }; +} +function BaseRRElementImpl(RRNodeClass) { + return class BaseRRElement extends RRNodeClass { + constructor(tagName) { + super(); + this.nodeType = NodeType.ELEMENT_NODE; + this.RRNodeType = NodeType$1.Element; + this.attributes = {}; + this.shadowRoot = null; + this.tagName = tagName.toUpperCase(); + this.nodeName = tagName.toUpperCase(); + } + get textContent() { + let result = ''; + this.childNodes.forEach((node) => (result += node.textContent)); + return result; + } + set textContent(textContent) { + this.firstChild = null; + this.lastChild = null; + this.appendChild(this.ownerDocument.createTextNode(textContent)); + } + get classList() { + return new ClassList(this.attributes.class, (newClassName) => { + this.attributes.class = newClassName; + }); + } + get id() { + return this.attributes.id || ''; + } + get className() { + return this.attributes.class || ''; + } + get style() { + const style = (this.attributes.style ? parseCSSText(this.attributes.style) : {}); + const hyphenateRE = /\B([A-Z])/g; + style.setProperty = (name, value, priority) => { + if (hyphenateRE.test(name)) + return; + const normalizedName = camelize(name); + if (!value) + delete style[normalizedName]; + else + style[normalizedName] = value; + if (priority === 'important') + style[normalizedName] += ' !important'; + this.attributes.style = toCSSText(style); + }; + style.removeProperty = (name) => { + if (hyphenateRE.test(name)) + return ''; + const normalizedName = camelize(name); + const value = style[normalizedName] || ''; + delete style[normalizedName]; + this.attributes.style = toCSSText(style); + return value; + }; + return style; + } + getAttribute(name) { + return this.attributes[name] || null; + } + setAttribute(name, attribute) { + this.attributes[name] = attribute; + } + setAttributeNS(_namespace, qualifiedName, value) { + this.setAttribute(qualifiedName, value); + } + removeAttribute(name) { + delete this.attributes[name]; + } + appendChild(newChild) { + return appendChild(this, newChild); + } + insertBefore(newChild, refChild) { + return insertBefore(this, newChild, refChild); + } + removeChild(node) { + return removeChild(this, node); + } + attachShadow(_init) { + const shadowRoot = this.ownerDocument.createElement('SHADOWROOT'); + this.shadowRoot = shadowRoot; + return shadowRoot; + } + dispatchEvent(_event) { + return true; + } + toString() { + let attributeString = ''; + for (const attribute in this.attributes) { + attributeString += `${attribute}="${this.attributes[attribute]}" `; + } + return `${this.tagName} ${attributeString}`; + } + }; +} +function BaseRRMediaElementImpl(RRElementClass) { + return class BaseRRMediaElement extends RRElementClass { + attachShadow(_init) { + throw new Error(`RRDomException: Failed to execute 'attachShadow' on 'RRElement': This RRElement does not support attachShadow`); + } + play() { + this.paused = false; + } + pause() { + this.paused = true; + } + }; +} +function BaseRRTextImpl(RRNodeClass) { + return class BaseRRText extends RRNodeClass { + constructor(data) { + super(); + this.nodeType = NodeType.TEXT_NODE; + this.nodeName = '#text'; + this.RRNodeType = NodeType$1.Text; + this.data = data; + } + get textContent() { + return this.data; + } + set textContent(textContent) { + this.data = textContent; + } + toString() { + return `RRText text=${JSON.stringify(this.data)}`; + } + }; +} +function BaseRRCommentImpl(RRNodeClass) { + return class BaseRRComment extends RRNodeClass { + constructor(data) { + super(); + this.nodeType = NodeType.COMMENT_NODE; + this.nodeName = '#comment'; + this.RRNodeType = NodeType$1.Comment; + this.data = data; + } + get textContent() { + return this.data; + } + set textContent(textContent) { + this.data = textContent; + } + toString() { + return `RRComment text=${JSON.stringify(this.data)}`; + } + }; +} +function BaseRRCDATASectionImpl(RRNodeClass) { + return class BaseRRCDATASection extends RRNodeClass { + constructor(data) { + super(); + this.nodeName = '#cdata-section'; + this.nodeType = NodeType.CDATA_SECTION_NODE; + this.RRNodeType = NodeType$1.CDATA; + this.data = data; + } + get textContent() { + return this.data; + } + set textContent(textContent) { + this.data = textContent; + } + toString() { + return `RRCDATASection data=${JSON.stringify(this.data)}`; + } + }; +} +class ClassList { + constructor(classText, onChange) { + this.classes = []; + this.add = (...classNames) => { + for (const item of classNames) { + const className = String(item); + if (this.classes.indexOf(className) >= 0) + continue; + this.classes.push(className); + } + this.onChange && this.onChange(this.classes.join(' ')); + }; + this.remove = (...classNames) => { + this.classes = this.classes.filter((item) => classNames.indexOf(item) === -1); + this.onChange && this.onChange(this.classes.join(' ')); + }; + if (classText) { + const classes = classText.trim().split(/\s+/); + this.classes.push(...classes); + } + this.onChange = onChange; + } +} +function appendChild(parent, newChild) { + if (newChild.parentNode) + newChild.parentNode.removeChild(newChild); + if (parent.lastChild) { + parent.lastChild.nextSibling = newChild; + newChild.previousSibling = parent.lastChild; + } + else { + parent.firstChild = newChild; + newChild.previousSibling = null; + } + parent.lastChild = newChild; + newChild.nextSibling = null; + newChild.parentNode = parent; + newChild.parentElement = parent; + newChild.ownerDocument = parent.ownerDocument; + return newChild; +} +function insertBefore(parent, newChild, refChild) { + if (!refChild) + return appendChild(parent, newChild); + if (refChild.parentNode !== parent) + throw new Error("Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode."); + if (newChild === refChild) + return newChild; + if (newChild.parentNode) + newChild.parentNode.removeChild(newChild); + newChild.previousSibling = refChild.previousSibling; + refChild.previousSibling = newChild; + newChild.nextSibling = refChild; + if (newChild.previousSibling) + newChild.previousSibling.nextSibling = newChild; + else + parent.firstChild = newChild; + newChild.parentElement = parent; + newChild.parentNode = parent; + newChild.ownerDocument = parent.ownerDocument; + return newChild; +} +function removeChild(parent, child) { + if (child.parentNode !== parent) + throw new Error("Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."); + if (child.previousSibling) + child.previousSibling.nextSibling = child.nextSibling; + else + parent.firstChild = child.nextSibling; + if (child.nextSibling) + child.nextSibling.previousSibling = child.previousSibling; + else + parent.lastChild = child.previousSibling; + child.previousSibling = null; + child.nextSibling = null; + child.parentElement = null; + child.parentNode = null; + return child; +} +var NodeType; +(function (NodeType) { + NodeType[NodeType["PLACEHOLDER"] = 0] = "PLACEHOLDER"; + NodeType[NodeType["ELEMENT_NODE"] = 1] = "ELEMENT_NODE"; + NodeType[NodeType["ATTRIBUTE_NODE"] = 2] = "ATTRIBUTE_NODE"; + NodeType[NodeType["TEXT_NODE"] = 3] = "TEXT_NODE"; + NodeType[NodeType["CDATA_SECTION_NODE"] = 4] = "CDATA_SECTION_NODE"; + NodeType[NodeType["ENTITY_REFERENCE_NODE"] = 5] = "ENTITY_REFERENCE_NODE"; + NodeType[NodeType["ENTITY_NODE"] = 6] = "ENTITY_NODE"; + NodeType[NodeType["PROCESSING_INSTRUCTION_NODE"] = 7] = "PROCESSING_INSTRUCTION_NODE"; + NodeType[NodeType["COMMENT_NODE"] = 8] = "COMMENT_NODE"; + NodeType[NodeType["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE"; + NodeType[NodeType["DOCUMENT_TYPE_NODE"] = 10] = "DOCUMENT_TYPE_NODE"; + NodeType[NodeType["DOCUMENT_FRAGMENT_NODE"] = 11] = "DOCUMENT_FRAGMENT_NODE"; +})(NodeType || (NodeType = {})); + +const NAMESPACES = { + svg: 'http://www.w3.org/2000/svg', + 'xlink:href': 'http://www.w3.org/1999/xlink', + xmlns: 'http://www.w3.org/2000/xmlns/', +}; +const SVGTagMap = { + altglyph: 'altGlyph', + altglyphdef: 'altGlyphDef', + altglyphitem: 'altGlyphItem', + animatecolor: 'animateColor', + animatemotion: 'animateMotion', + animatetransform: 'animateTransform', + clippath: 'clipPath', + feblend: 'feBlend', + fecolormatrix: 'feColorMatrix', + fecomponenttransfer: 'feComponentTransfer', + fecomposite: 'feComposite', + feconvolvematrix: 'feConvolveMatrix', + fediffuselighting: 'feDiffuseLighting', + fedisplacementmap: 'feDisplacementMap', + fedistantlight: 'feDistantLight', + fedropshadow: 'feDropShadow', + feflood: 'feFlood', + fefunca: 'feFuncA', + fefuncb: 'feFuncB', + fefuncg: 'feFuncG', + fefuncr: 'feFuncR', + fegaussianblur: 'feGaussianBlur', + feimage: 'feImage', + femerge: 'feMerge', + femergenode: 'feMergeNode', + femorphology: 'feMorphology', + feoffset: 'feOffset', + fepointlight: 'fePointLight', + fespecularlighting: 'feSpecularLighting', + fespotlight: 'feSpotLight', + fetile: 'feTile', + feturbulence: 'feTurbulence', + foreignobject: 'foreignObject', + glyphref: 'glyphRef', + lineargradient: 'linearGradient', + radialgradient: 'radialGradient', +}; +let createdNodeSet = null; +function diff(oldTree, newTree, replayer, rrnodeMirror = newTree.mirror || + newTree.ownerDocument.mirror) { + oldTree = diffBeforeUpdatingChildren(oldTree, newTree, replayer, rrnodeMirror); + diffChildren(oldTree, newTree, replayer, rrnodeMirror); + diffAfterUpdatingChildren(oldTree, newTree, replayer); +} +function diffBeforeUpdatingChildren(oldTree, newTree, replayer, rrnodeMirror) { + var _a; + if (replayer.afterAppend && !createdNodeSet) { + createdNodeSet = new WeakSet(); + setTimeout(() => { + createdNodeSet = null; + }, 0); + } + if (!sameNodeType(oldTree, newTree)) { + const calibratedOldTree = createOrGetNode(newTree, replayer.mirror, rrnodeMirror); + (_a = oldTree.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(calibratedOldTree, oldTree); + oldTree = calibratedOldTree; + } + switch (newTree.RRNodeType) { + case NodeType$1.Document: { + if (!nodeMatching(oldTree, newTree, replayer.mirror, rrnodeMirror)) { + const newMeta = rrnodeMirror.getMeta(newTree); + if (newMeta) { + replayer.mirror.removeNodeFromMap(oldTree); + oldTree.close(); + oldTree.open(); + replayer.mirror.add(oldTree, newMeta); + createdNodeSet === null || createdNodeSet === void 0 ? void 0 : createdNodeSet.add(oldTree); + } + } + break; + } + case NodeType$1.Element: { + const oldElement = oldTree; + const newRRElement = newTree; + switch (newRRElement.tagName) { + case 'IFRAME': { + const oldContentDocument = oldTree + .contentDocument; + if (!oldContentDocument) + break; + diff(oldContentDocument, newTree.contentDocument, replayer, rrnodeMirror); + break; + } + } + if (newRRElement.shadowRoot) { + if (!oldElement.shadowRoot) + oldElement.attachShadow({ mode: 'open' }); + diffChildren(oldElement.shadowRoot, newRRElement.shadowRoot, replayer, rrnodeMirror); + } + diffProps(oldElement, newRRElement, rrnodeMirror); + break; + } + } + return oldTree; +} +function diffAfterUpdatingChildren(oldTree, newTree, replayer) { + var _a; + switch (newTree.RRNodeType) { + case NodeType$1.Document: { + const scrollData = newTree.scrollData; + scrollData && replayer.applyScroll(scrollData, true); + break; + } + case NodeType$1.Element: { + const oldElement = oldTree; + const newRRElement = newTree; + newRRElement.scrollData && + replayer.applyScroll(newRRElement.scrollData, true); + newRRElement.inputData && replayer.applyInput(newRRElement.inputData); + switch (newRRElement.tagName) { + case 'AUDIO': + case 'VIDEO': { + const oldMediaElement = oldTree; + const newMediaRRElement = newRRElement; + if (newMediaRRElement.paused !== undefined) + newMediaRRElement.paused + ? void oldMediaElement.pause() + : void oldMediaElement.play(); + if (newMediaRRElement.muted !== undefined) + oldMediaElement.muted = newMediaRRElement.muted; + if (newMediaRRElement.volume !== undefined) + oldMediaElement.volume = newMediaRRElement.volume; + if (newMediaRRElement.currentTime !== undefined) + oldMediaElement.currentTime = newMediaRRElement.currentTime; + if (newMediaRRElement.playbackRate !== undefined) + oldMediaElement.playbackRate = newMediaRRElement.playbackRate; + if (newMediaRRElement.loop !== undefined) + oldMediaElement.loop = newMediaRRElement.loop; + break; + } + case 'CANVAS': { + const rrCanvasElement = newTree; + if (rrCanvasElement.rr_dataURL !== null) { + const image = document.createElement('img'); + image.onload = () => { + const ctx = oldElement.getContext('2d'); + if (ctx) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } + }; + image.src = rrCanvasElement.rr_dataURL; + } + rrCanvasElement.canvasMutations.forEach((canvasMutation) => replayer.applyCanvas(canvasMutation.event, canvasMutation.mutation, oldTree)); + break; + } + case 'STYLE': { + const styleSheet = oldElement.sheet; + styleSheet && + newTree.rules.forEach((data) => replayer.applyStyleSheetMutation(data, styleSheet)); + break; + } + } + break; + } + case NodeType$1.Text: + case NodeType$1.Comment: + case NodeType$1.CDATA: { + if (oldTree.textContent !== + newTree.data) + oldTree.textContent = newTree.data; + break; + } + } + if (createdNodeSet === null || createdNodeSet === void 0 ? void 0 : createdNodeSet.has(oldTree)) { + createdNodeSet.delete(oldTree); + (_a = replayer.afterAppend) === null || _a === void 0 ? void 0 : _a.call(replayer, oldTree, replayer.mirror.getId(oldTree)); + } +} +function diffProps(oldTree, newTree, rrnodeMirror) { + const oldAttributes = oldTree.attributes; + const newAttributes = newTree.attributes; + for (const name in newAttributes) { + const newValue = newAttributes[name]; + const sn = rrnodeMirror.getMeta(newTree); + if ((sn === null || sn === void 0 ? void 0 : sn.isSVG) && NAMESPACES[name]) + oldTree.setAttributeNS(NAMESPACES[name], name, newValue); + else if (newTree.tagName === 'CANVAS' && name === 'rr_dataURL') { + const image = document.createElement('img'); + image.src = newValue; + image.onload = () => { + const ctx = oldTree.getContext('2d'); + if (ctx) { + ctx.drawImage(image, 0, 0, image.width, image.height); + } + }; + } + else if (newTree.tagName === 'IFRAME' && name === 'srcdoc') + continue; + else + oldTree.setAttribute(name, newValue); + } + for (const { name } of Array.from(oldAttributes)) + if (!(name in newAttributes)) + oldTree.removeAttribute(name); + newTree.scrollLeft && (oldTree.scrollLeft = newTree.scrollLeft); + newTree.scrollTop && (oldTree.scrollTop = newTree.scrollTop); +} +function diffChildren(oldTree, newTree, replayer, rrnodeMirror) { + const oldChildren = Array.from(oldTree.childNodes); + const newChildren = newTree.childNodes; + if (oldChildren.length === 0 && newChildren.length === 0) + return; + let oldStartIndex = 0, oldEndIndex = oldChildren.length - 1, newStartIndex = 0, newEndIndex = newChildren.length - 1; + let oldStartNode = oldChildren[oldStartIndex], oldEndNode = oldChildren[oldEndIndex], newStartNode = newChildren[newStartIndex], newEndNode = newChildren[newEndIndex]; + let oldIdToIndex = undefined, indexInOld = undefined; + while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { + if (oldStartNode === undefined) { + oldStartNode = oldChildren[++oldStartIndex]; + } + else if (oldEndNode === undefined) { + oldEndNode = oldChildren[--oldEndIndex]; + } + else if (nodeMatching(oldStartNode, newStartNode, replayer.mirror, rrnodeMirror)) { + oldStartNode = oldChildren[++oldStartIndex]; + newStartNode = newChildren[++newStartIndex]; + } + else if (nodeMatching(oldEndNode, newEndNode, replayer.mirror, rrnodeMirror)) { + oldEndNode = oldChildren[--oldEndIndex]; + newEndNode = newChildren[--newEndIndex]; + } + else if (nodeMatching(oldStartNode, newEndNode, replayer.mirror, rrnodeMirror)) { + try { + oldTree.insertBefore(oldStartNode, oldEndNode.nextSibling); + } + catch (e) { + console.warn(e); + } + oldStartNode = oldChildren[++oldStartIndex]; + newEndNode = newChildren[--newEndIndex]; + } + else if (nodeMatching(oldEndNode, newStartNode, replayer.mirror, rrnodeMirror)) { + try { + oldTree.insertBefore(oldEndNode, oldStartNode); + } + catch (e) { + console.warn(e); + } + oldEndNode = oldChildren[--oldEndIndex]; + newStartNode = newChildren[++newStartIndex]; + } + else { + if (!oldIdToIndex) { + oldIdToIndex = {}; + for (let i = oldStartIndex; i <= oldEndIndex; i++) { + const oldChild = oldChildren[i]; + if (oldChild && replayer.mirror.hasNode(oldChild)) + oldIdToIndex[replayer.mirror.getId(oldChild)] = i; + } + } + indexInOld = oldIdToIndex[rrnodeMirror.getId(newStartNode)]; + const nodeToMove = oldChildren[indexInOld]; + if (indexInOld !== undefined && + nodeToMove && + nodeMatching(nodeToMove, newStartNode, replayer.mirror, rrnodeMirror)) { + try { + oldTree.insertBefore(nodeToMove, oldStartNode); + } + catch (e) { + console.warn(e); + } + oldChildren[indexInOld] = undefined; + } + else { + const newNode = createOrGetNode(newStartNode, replayer.mirror, rrnodeMirror); + if (oldTree.nodeName === '#document' && + oldStartNode && + ((newNode.nodeType === newNode.DOCUMENT_TYPE_NODE && + oldStartNode.nodeType === oldStartNode.DOCUMENT_TYPE_NODE) || + (newNode.nodeType === newNode.ELEMENT_NODE && + oldStartNode.nodeType === oldStartNode.ELEMENT_NODE))) { + oldTree.removeChild(oldStartNode); + replayer.mirror.removeNodeFromMap(oldStartNode); + oldStartNode = oldChildren[++oldStartIndex]; + } + try { + oldTree.insertBefore(newNode, oldStartNode || null); + } + catch (e) { + console.warn(e); + } + } + newStartNode = newChildren[++newStartIndex]; + } + } + if (oldStartIndex > oldEndIndex) { + const referenceRRNode = newChildren[newEndIndex + 1]; + let referenceNode = null; + if (referenceRRNode) + referenceNode = replayer.mirror.getNode(rrnodeMirror.getId(referenceRRNode)); + for (; newStartIndex <= newEndIndex; ++newStartIndex) { + const newNode = createOrGetNode(newChildren[newStartIndex], replayer.mirror, rrnodeMirror); + try { + oldTree.insertBefore(newNode, referenceNode); + } + catch (e) { + console.warn(e); + } + } + } + else if (newStartIndex > newEndIndex) { + for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { + const node = oldChildren[oldStartIndex]; + if (!node || node.parentNode !== oldTree) + continue; + try { + oldTree.removeChild(node); + replayer.mirror.removeNodeFromMap(node); + } + catch (e) { + console.warn(e); + } + } + } + let oldChild = oldTree.firstChild; + let newChild = newTree.firstChild; + while (oldChild !== null && newChild !== null) { + diff(oldChild, newChild, replayer, rrnodeMirror); + oldChild = oldChild.nextSibling; + newChild = newChild.nextSibling; + } +} +function createOrGetNode(rrNode, domMirror, rrnodeMirror) { + const nodeId = rrnodeMirror.getId(rrNode); + const sn = rrnodeMirror.getMeta(rrNode); + let node = null; + if (nodeId > -1) + node = domMirror.getNode(nodeId); + if (node !== null && sameNodeType(node, rrNode)) + return node; + switch (rrNode.RRNodeType) { + case NodeType$1.Document: + node = new Document(); + break; + case NodeType$1.DocumentType: + node = document.implementation.createDocumentType(rrNode.name, rrNode.publicId, rrNode.systemId); + break; + case NodeType$1.Element: { + let tagName = rrNode.tagName.toLowerCase(); + tagName = SVGTagMap[tagName] || tagName; + if (sn && 'isSVG' in sn && (sn === null || sn === void 0 ? void 0 : sn.isSVG)) { + node = document.createElementNS(NAMESPACES['svg'], tagName); + } + else + node = document.createElement(rrNode.tagName); + break; + } + case NodeType$1.Text: + node = document.createTextNode(rrNode.data); + break; + case NodeType$1.Comment: + node = document.createComment(rrNode.data); + break; + case NodeType$1.CDATA: + node = document.createCDATASection(rrNode.data); + break; + } + if (sn) + domMirror.add(node, Object.assign({}, sn)); + try { + createdNodeSet === null || createdNodeSet === void 0 ? void 0 : createdNodeSet.add(node); + } + catch (e) { + } + return node; +} +function sameNodeType(node1, node2) { + if (node1.nodeType !== node2.nodeType) + return false; + return (node1.nodeType !== node1.ELEMENT_NODE || + node1.tagName.toUpperCase() === + node2.tagName); +} +function nodeMatching(node1, node2, domMirror, rrdomMirror) { + const node1Id = domMirror.getId(node1); + const node2Id = rrdomMirror.getId(node2); + if (node1Id === -1 || node1Id !== node2Id) + return false; + return sameNodeType(node1, node2); +} + +class RRDocument extends BaseRRDocumentImpl(BaseRRNode) { + get unserializedId() { + return this._unserializedId--; + } + constructor(mirror) { + super(); + this.UNSERIALIZED_STARTING_ID = -2; + this._unserializedId = this.UNSERIALIZED_STARTING_ID; + this.mirror = createMirror(); + this.scrollData = null; + if (mirror) { + this.mirror = mirror; + } + } + createDocument(_namespace, _qualifiedName, _doctype) { + return new RRDocument(); + } + createDocumentType(qualifiedName, publicId, systemId) { + const documentTypeNode = new RRDocumentType(qualifiedName, publicId, systemId); + documentTypeNode.ownerDocument = this; + return documentTypeNode; + } + createElement(tagName) { + const upperTagName = tagName.toUpperCase(); + let element; + switch (upperTagName) { + case 'AUDIO': + case 'VIDEO': + element = new RRMediaElement(upperTagName); + break; + case 'IFRAME': + element = new RRIFrameElement(upperTagName, this.mirror); + break; + case 'CANVAS': + element = new RRCanvasElement(upperTagName); + break; + case 'STYLE': + element = new RRStyleElement(upperTagName); + break; + default: + element = new RRElement(upperTagName); + break; + } + element.ownerDocument = this; + return element; + } + createComment(data) { + const commentNode = new RRComment(data); + commentNode.ownerDocument = this; + return commentNode; + } + createCDATASection(data) { + const sectionNode = new RRCDATASection(data); + sectionNode.ownerDocument = this; + return sectionNode; + } + createTextNode(data) { + const textNode = new RRText(data); + textNode.ownerDocument = this; + return textNode; + } + destroyTree() { + this.firstChild = null; + this.lastChild = null; + this.mirror.reset(); + } + open() { + super.open(); + this._unserializedId = this.UNSERIALIZED_STARTING_ID; + } +} +const RRDocumentType = BaseRRDocumentTypeImpl(BaseRRNode); +class RRElement extends BaseRRElementImpl(BaseRRNode) { + constructor() { + super(...arguments); + this.inputData = null; + this.scrollData = null; + } +} +class RRMediaElement extends BaseRRMediaElementImpl(RRElement) { +} +class RRCanvasElement extends RRElement { + constructor() { + super(...arguments); + this.rr_dataURL = null; + this.canvasMutations = []; + } + getContext() { + return null; + } +} +class RRStyleElement extends RRElement { + constructor() { + super(...arguments); + this.rules = []; + } +} +class RRIFrameElement extends RRElement { + constructor(upperTagName, mirror) { + super(upperTagName); + this.contentDocument = new RRDocument(); + this.contentDocument.mirror = mirror; + } +} +const RRText = BaseRRTextImpl(BaseRRNode); +const RRComment = BaseRRCommentImpl(BaseRRNode); +const RRCDATASection = BaseRRCDATASectionImpl(BaseRRNode); +function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'FORM'; + } + return element.tagName.toUpperCase(); +} +function buildFromNode(node, rrdom, domMirror, parentRRNode) { + let rrNode; + switch (node.nodeType) { + case NodeType.DOCUMENT_NODE: + if (parentRRNode && parentRRNode.nodeName === 'IFRAME') + rrNode = parentRRNode.contentDocument; + else { + rrNode = rrdom; + rrNode.compatMode = node.compatMode; + } + break; + case NodeType.DOCUMENT_TYPE_NODE: { + const documentType = node; + rrNode = rrdom.createDocumentType(documentType.name, documentType.publicId, documentType.systemId); + break; + } + case NodeType.ELEMENT_NODE: { + const elementNode = node; + const tagName = getValidTagName(elementNode); + rrNode = rrdom.createElement(tagName); + const rrElement = rrNode; + for (const { name, value } of Array.from(elementNode.attributes)) { + rrElement.attributes[name] = value; + } + elementNode.scrollLeft && (rrElement.scrollLeft = elementNode.scrollLeft); + elementNode.scrollTop && (rrElement.scrollTop = elementNode.scrollTop); + break; + } + case NodeType.TEXT_NODE: + rrNode = rrdom.createTextNode(node.textContent || ''); + break; + case NodeType.CDATA_SECTION_NODE: + rrNode = rrdom.createCDATASection(node.data); + break; + case NodeType.COMMENT_NODE: + rrNode = rrdom.createComment(node.textContent || ''); + break; + case NodeType.DOCUMENT_FRAGMENT_NODE: + rrNode = parentRRNode.attachShadow({ mode: 'open' }); + break; + default: + return null; + } + let sn = domMirror.getMeta(node); + if (rrdom instanceof RRDocument) { + if (!sn) { + sn = getDefaultSN(rrNode, rrdom.unserializedId); + domMirror.add(node, sn); + } + rrdom.mirror.add(rrNode, Object.assign({}, sn)); + } + return rrNode; +} +function buildFromDom(dom, domMirror = createMirror$1(), rrdom = new RRDocument()) { + function walk(node, parentRRNode) { + const rrNode = buildFromNode(node, rrdom, domMirror, parentRRNode); + if (rrNode === null) + return; + if ((parentRRNode === null || parentRRNode === void 0 ? void 0 : parentRRNode.nodeName) !== 'IFRAME' && + node.nodeType !== NodeType.DOCUMENT_FRAGMENT_NODE) { + parentRRNode === null || parentRRNode === void 0 ? void 0 : parentRRNode.appendChild(rrNode); + rrNode.parentNode = parentRRNode; + rrNode.parentElement = parentRRNode; + } + if (node.nodeName === 'IFRAME') { + const iframeDoc = node.contentDocument; + iframeDoc && walk(iframeDoc, rrNode); + } + else if (node.nodeType === NodeType.DOCUMENT_NODE || + node.nodeType === NodeType.ELEMENT_NODE || + node.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE) { + if (node.nodeType === NodeType.ELEMENT_NODE && + node.shadowRoot) + walk(node.shadowRoot, rrNode); + node.childNodes.forEach((childNode) => walk(childNode, rrNode)); + } + } + walk(dom, null); + return rrdom; +} +function createMirror() { + return new Mirror(); +} +class Mirror { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } +} +function getDefaultSN(node, id) { + switch (node.RRNodeType) { + case NodeType$1.Document: + return { + id, + type: node.RRNodeType, + childNodes: [], + }; + case NodeType$1.DocumentType: { + const doctype = node; + return { + id, + type: node.RRNodeType, + name: doctype.name, + publicId: doctype.publicId, + systemId: doctype.systemId, + }; + } + case NodeType$1.Element: + return { + id, + type: node.RRNodeType, + tagName: node.tagName.toLowerCase(), + attributes: {}, + childNodes: [], + }; + case NodeType$1.Text: + return { + id, + type: node.RRNodeType, + textContent: node.textContent || '', + }; + case NodeType$1.Comment: + return { + id, + type: node.RRNodeType, + textContent: node.textContent || '', + }; + case NodeType$1.CDATA: + return { + id, + type: node.RRNodeType, + textContent: '', + }; + } +} + +function mitt$1(n){return {all:n=n||new Map,on:function(t,e){var i=n.get(t);i?i.push(e):n.set(t,[e]);},off:function(t,e){var i=n.get(t);i&&(e?i.splice(i.indexOf(e)>>>0,1):n.set(t,[]));},emit:function(t,e){var i=n.get(t);i&&i.slice().map(function(n){n(e);}),(i=n.get("*"))&&i.slice().map(function(n){n(t,e);});}}} + +var mittProxy = /*#__PURE__*/Object.freeze({ + __proto__: null, + 'default': mitt$1 +}); + +function polyfill(w = window, d = document) { + if ('scrollBehavior' in d.documentElement.style && + w.__forceSmoothScrollPolyfill__ !== true) { + return; + } + const Element = w.HTMLElement || w.Element; + const SCROLL_TIME = 468; + const original = { + scroll: w.scroll || w.scrollTo, + scrollBy: w.scrollBy, + elementScroll: Element.prototype.scroll || scrollElement, + scrollIntoView: Element.prototype.scrollIntoView, + }; + const now = w.performance && w.performance.now + ? w.performance.now.bind(w.performance) + : Date.now; + function isMicrosoftBrowser(userAgent) { + const userAgentPatterns = ['MSIE ', 'Trident/', 'Edge/']; + return new RegExp(userAgentPatterns.join('|')).test(userAgent); + } + const ROUNDING_TOLERANCE = isMicrosoftBrowser(w.navigator.userAgent) ? 1 : 0; + function scrollElement(x, y) { + this.scrollLeft = x; + this.scrollTop = y; + } + function ease(k) { + return 0.5 * (1 - Math.cos(Math.PI * k)); + } + function shouldBailOut(firstArg) { + if (firstArg === null || + typeof firstArg !== 'object' || + firstArg.behavior === undefined || + firstArg.behavior === 'auto' || + firstArg.behavior === 'instant') { + return true; + } + if (typeof firstArg === 'object' && firstArg.behavior === 'smooth') { + return false; + } + throw new TypeError('behavior member of ScrollOptions ' + + firstArg.behavior + + ' is not a valid value for enumeration ScrollBehavior.'); + } + function hasScrollableSpace(el, axis) { + if (axis === 'Y') { + return el.clientHeight + ROUNDING_TOLERANCE < el.scrollHeight; + } + if (axis === 'X') { + return el.clientWidth + ROUNDING_TOLERANCE < el.scrollWidth; + } + } + function canOverflow(el, axis) { + const overflowValue = w.getComputedStyle(el, null)['overflow' + axis]; + return overflowValue === 'auto' || overflowValue === 'scroll'; + } + function isScrollable(el) { + const isScrollableY = hasScrollableSpace(el, 'Y') && canOverflow(el, 'Y'); + const isScrollableX = hasScrollableSpace(el, 'X') && canOverflow(el, 'X'); + return isScrollableY || isScrollableX; + } + function findScrollableParent(el) { + while (el !== d.body && isScrollable(el) === false) { + el = el.parentNode || el.host; + } + return el; + } + function step(context) { + const time = now(); + let value; + let currentX; + let currentY; + let elapsed = (time - context.startTime) / SCROLL_TIME; + elapsed = elapsed > 1 ? 1 : elapsed; + value = ease(elapsed); + currentX = context.startX + (context.x - context.startX) * value; + currentY = context.startY + (context.y - context.startY) * value; + context.method.call(context.scrollable, currentX, currentY); + if (currentX !== context.x || currentY !== context.y) { + w.requestAnimationFrame(step.bind(w, context)); + } + } + function smoothScroll(el, x, y) { + let scrollable; + let startX; + let startY; + let method; + const startTime = now(); + if (el === d.body) { + scrollable = w; + startX = w.scrollX || w.pageXOffset; + startY = w.scrollY || w.pageYOffset; + method = original.scroll; + } + else { + scrollable = el; + startX = el.scrollLeft; + startY = el.scrollTop; + method = scrollElement; + } + step({ + scrollable: scrollable, + method: method, + startTime: startTime, + startX: startX, + startY: startY, + x: x, + y: y, + }); + } + w.scroll = w.scrollTo = function () { + if (arguments[0] === undefined) { + return; + } + if (shouldBailOut(arguments[0]) === true) { + original.scroll.call(w, arguments[0].left !== undefined + ? arguments[0].left + : typeof arguments[0] !== 'object' + ? arguments[0] + : w.scrollX || w.pageXOffset, arguments[0].top !== undefined + ? arguments[0].top + : arguments[1] !== undefined + ? arguments[1] + : w.scrollY || w.pageYOffset); + return; + } + smoothScroll.call(w, d.body, arguments[0].left !== undefined + ? ~~arguments[0].left + : w.scrollX || w.pageXOffset, arguments[0].top !== undefined + ? ~~arguments[0].top + : w.scrollY || w.pageYOffset); + }; + w.scrollBy = function () { + if (arguments[0] === undefined) { + return; + } + if (shouldBailOut(arguments[0])) { + original.scrollBy.call(w, arguments[0].left !== undefined + ? arguments[0].left + : typeof arguments[0] !== 'object' + ? arguments[0] + : 0, arguments[0].top !== undefined + ? arguments[0].top + : arguments[1] !== undefined + ? arguments[1] + : 0); + return; + } + smoothScroll.call(w, d.body, ~~arguments[0].left + (w.scrollX || w.pageXOffset), ~~arguments[0].top + (w.scrollY || w.pageYOffset)); + }; + Element.prototype.scroll = Element.prototype.scrollTo = function () { + if (arguments[0] === undefined) { + return; + } + if (shouldBailOut(arguments[0]) === true) { + if (typeof arguments[0] === 'number' && arguments[1] === undefined) { + throw new SyntaxError('Value could not be converted'); + } + original.elementScroll.call(this, arguments[0].left !== undefined + ? ~~arguments[0].left + : typeof arguments[0] !== 'object' + ? ~~arguments[0] + : this.scrollLeft, arguments[0].top !== undefined + ? ~~arguments[0].top + : arguments[1] !== undefined + ? ~~arguments[1] + : this.scrollTop); + return; + } + const left = arguments[0].left; + const top = arguments[0].top; + smoothScroll.call(this, this, typeof left === 'undefined' ? this.scrollLeft : ~~left, typeof top === 'undefined' ? this.scrollTop : ~~top); + }; + Element.prototype.scrollBy = function () { + if (arguments[0] === undefined) { + return; + } + if (shouldBailOut(arguments[0]) === true) { + original.elementScroll.call(this, arguments[0].left !== undefined + ? ~~arguments[0].left + this.scrollLeft + : ~~arguments[0] + this.scrollLeft, arguments[0].top !== undefined + ? ~~arguments[0].top + this.scrollTop + : ~~arguments[1] + this.scrollTop); + return; + } + this.scroll({ + left: ~~arguments[0].left + this.scrollLeft, + top: ~~arguments[0].top + this.scrollTop, + behavior: arguments[0].behavior, + }); + }; + Element.prototype.scrollIntoView = function () { + if (shouldBailOut(arguments[0]) === true) { + original.scrollIntoView.call(this, arguments[0] === undefined ? true : arguments[0]); + return; + } + const scrollableParent = findScrollableParent(this); + const parentRects = scrollableParent.getBoundingClientRect(); + const clientRects = this.getBoundingClientRect(); + if (scrollableParent !== d.body) { + smoothScroll.call(this, scrollableParent, scrollableParent.scrollLeft + clientRects.left - parentRects.left, scrollableParent.scrollTop + clientRects.top - parentRects.top); + if (w.getComputedStyle(scrollableParent).position !== 'fixed') { + w.scrollBy({ + left: parentRects.left, + top: parentRects.top, + behavior: 'smooth', + }); + } + } + else { + w.scrollBy({ + left: clientRects.left, + top: clientRects.top, + behavior: 'smooth', + }); + } + }; +} + +class Timer { + constructor(actions = [], config) { + this.timeOffset = 0; + this.raf = null; + this.actions = actions; + this.speed = config.speed; + } + addAction(action) { + const rafWasActive = this.raf === true; + if (!this.actions.length || + this.actions[this.actions.length - 1].delay <= action.delay) { + this.actions.push(action); + } + else { + const index = this.findActionIndex(action); + this.actions.splice(index, 0, action); + } + if (rafWasActive) { + this.raf = requestAnimationFrame(this.rafCheck.bind(this)); + } + } + start() { + this.timeOffset = 0; + this.lastTimestamp = performance.now(); + this.raf = requestAnimationFrame(this.rafCheck.bind(this)); + } + rafCheck() { + const time = performance.now(); + this.timeOffset += (time - this.lastTimestamp) * this.speed; + this.lastTimestamp = time; + while (this.actions.length) { + const action = this.actions[0]; + if (this.timeOffset >= action.delay) { + this.actions.shift(); + action.doAction(); + } + else { + break; + } + } + if (this.actions.length > 0) { + this.raf = requestAnimationFrame(this.rafCheck.bind(this)); + } + else { + this.raf = true; + } + } + clear() { + if (this.raf) { + if (this.raf !== true) { + cancelAnimationFrame(this.raf); + } + this.raf = null; + } + this.actions.length = 0; + } + setSpeed(speed) { + this.speed = speed; + } + isActive() { + return this.raf !== null; + } + findActionIndex(action) { + let start = 0; + let end = this.actions.length - 1; + while (start <= end) { + const mid = Math.floor((start + end) / 2); + if (this.actions[mid].delay < action.delay) { + start = mid + 1; + } + else if (this.actions[mid].delay > action.delay) { + end = mid - 1; + } + else { + return mid + 1; + } + } + return start; + } +} +function addDelay(event, baselineTime) { + if (event.type === EventType.IncrementalSnapshot && + event.data.source === IncrementalSource.MouseMove && + event.data.positions && + event.data.positions.length) { + const firstOffset = event.data.positions[0].timeOffset; + const firstTimestamp = event.timestamp + firstOffset; + event.delay = firstTimestamp - baselineTime; + return firstTimestamp - baselineTime; + } + event.delay = event.timestamp - baselineTime; + return event.delay; +} + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +function t(t,n){var e="function"==typeof Symbol&&t[Symbol.iterator];if(!e)return t;var r,o,i=e.call(t),a=[];try{for(;(void 0===n||n-- >0)&&!(r=i.next()).done;)a.push(r.value);}catch(t){o={error:t};}finally{try{r&&!r.done&&(e=i.return)&&e.call(i);}finally{if(o)throw o.error}}return a}var n;!function(t){t[t.NotStarted=0]="NotStarted",t[t.Running=1]="Running",t[t.Stopped=2]="Stopped";}(n||(n={}));var e={type:"xstate.init"};function r(t){return void 0===t?[]:[].concat(t)}function o(t){return {type:"xstate.assign",assignment:t}}function i$1(t,n){return "string"==typeof(t="string"==typeof t&&n&&n[t]?n[t]:t)?{type:t}:"function"==typeof t?{type:t.name,exec:t}:t}function a(t){return function(n){return t===n}}function u(t){return "string"==typeof t?{type:t}:t}function c(t,n){return {value:t,context:n,actions:[],changed:!1,matches:a(t)}}function f(t,n,e){var r=n,o=!1;return [t.filter((function(t){if("xstate.assign"===t.type){o=!0;var n=Object.assign({},r);return "function"==typeof t.assignment?n=t.assignment(r,e):Object.keys(t.assignment).forEach((function(o){n[o]="function"==typeof t.assignment[o]?t.assignment[o](r,e):t.assignment[o];})),r=n,!1}return !0})),r,o]}function s(n,o){void 0===o&&(o={});var s=t(f(r(n.states[n.initial].entry).map((function(t){return i$1(t,o.actions)})),n.context,e),2),l=s[0],v=s[1],y={config:n,_options:o,initialState:{value:n.initial,actions:l,context:v,matches:a(n.initial)},transition:function(e,o){var s,l,v="string"==typeof e?{value:e,context:n.context}:e,p=v.value,g=v.context,d=u(o),x=n.states[p];if(x.on){var m=r(x.on[d.type]);try{for(var h=function(t){var n="function"==typeof Symbol&&Symbol.iterator,e=n&&t[n],r=0;if(e)return e.call(t);if(t&&"number"==typeof t.length)return {next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")}(m),b=h.next();!b.done;b=h.next()){var S=b.value;if(void 0===S)return c(p,g);var w="string"==typeof S?{target:S}:S,j=w.target,E=w.actions,R=void 0===E?[]:E,N=w.cond,O=void 0===N?function(){return !0}:N,_=void 0===j,k=null!=j?j:p,T=n.states[k];if(O(g,d)){var q=t(f((_?r(R):[].concat(x.exit,R,T.entry).filter((function(t){return t}))).map((function(t){return i$1(t,y._options.actions)})),g,d),3),z=q[0],A=q[1],B=q[2],C=null!=j?j:p;return {value:C,context:A,actions:z,changed:j!==p||z.length>0||B,matches:a(C)}}}}catch(t){s={error:t};}finally{try{b&&!b.done&&(l=h.return)&&l.call(h);}finally{if(s)throw s.error}}}return c(p,g)}};return y}var l=function(t,n){return t.actions.forEach((function(e){var r=e.exec;return r&&r(t.context,n)}))};function v(t){var r=t.initialState,o=n.NotStarted,i=new Set,c={_machine:t,send:function(e){o===n.Running&&(r=t.transition(r,e),l(r,u(e)),i.forEach((function(t){return t(r)})));},subscribe:function(t){return i.add(t),t(r),{unsubscribe:function(){return i.delete(t)}}},start:function(i){if(i){var u="object"==typeof i?i:{context:t.config.context,value:i};r={value:u.value,actions:[],context:u.context,matches:a(u.value)};}return o=n.Running,l(r,e),c},stop:function(){return o=n.Stopped,i.clear(),c},get state(){return r},get status(){return o}};return c} + +function discardPriorSnapshots(events, baselineTime) { + for (let idx = events.length - 1; idx >= 0; idx--) { + const event = events[idx]; + if (event.type === EventType.Meta) { + if (event.timestamp <= baselineTime) { + return events.slice(idx); + } + } + } + return events; +} +function createPlayerService(context, { getCastFn, applyEventsSynchronously, emitter }) { + const playerMachine = s({ + id: 'player', + context, + initial: 'paused', + states: { + playing: { + on: { + PAUSE: { + target: 'paused', + actions: ['pause'], + }, + CAST_EVENT: { + target: 'playing', + actions: 'castEvent', + }, + END: { + target: 'paused', + actions: ['resetLastPlayedEvent', 'pause'], + }, + ADD_EVENT: { + target: 'playing', + actions: ['addEvent'], + }, + }, + }, + paused: { + on: { + PLAY: { + target: 'playing', + actions: ['recordTimeOffset', 'play'], + }, + CAST_EVENT: { + target: 'paused', + actions: 'castEvent', + }, + TO_LIVE: { + target: 'live', + actions: ['startLive'], + }, + ADD_EVENT: { + target: 'paused', + actions: ['addEvent'], + }, + }, + }, + live: { + on: { + ADD_EVENT: { + target: 'live', + actions: ['addEvent'], + }, + CAST_EVENT: { + target: 'live', + actions: ['castEvent'], + }, + }, + }, + }, + }, { + actions: { + castEvent: o({ + lastPlayedEvent: (ctx, event) => { + if (event.type === 'CAST_EVENT') { + return event.payload.event; + } + return ctx.lastPlayedEvent; + }, + }), + recordTimeOffset: o((ctx, event) => { + let timeOffset = ctx.timeOffset; + if ('payload' in event && 'timeOffset' in event.payload) { + timeOffset = event.payload.timeOffset; + } + return Object.assign(Object.assign({}, ctx), { timeOffset, baselineTime: ctx.events[0].timestamp + timeOffset }); + }), + play(ctx) { + var _a; + const { timer, events, baselineTime, lastPlayedEvent } = ctx; + timer.clear(); + for (const event of events) { + addDelay(event, baselineTime); + } + const neededEvents = discardPriorSnapshots(events, baselineTime); + let lastPlayedTimestamp = lastPlayedEvent === null || lastPlayedEvent === void 0 ? void 0 : lastPlayedEvent.timestamp; + if ((lastPlayedEvent === null || lastPlayedEvent === void 0 ? void 0 : lastPlayedEvent.type) === EventType.IncrementalSnapshot && + lastPlayedEvent.data.source === IncrementalSource.MouseMove) { + lastPlayedTimestamp = + lastPlayedEvent.timestamp + + ((_a = lastPlayedEvent.data.positions[0]) === null || _a === void 0 ? void 0 : _a.timeOffset); + } + if (baselineTime < (lastPlayedTimestamp || 0)) { + emitter.emit(ReplayerEvents.PlayBack); + } + const syncEvents = new Array(); + for (const event of neededEvents) { + if (lastPlayedTimestamp && + lastPlayedTimestamp < baselineTime && + (event.timestamp <= lastPlayedTimestamp || + event === lastPlayedEvent)) { + continue; + } + if (event.timestamp < baselineTime) { + syncEvents.push(event); + } + else { + const castFn = getCastFn(event, false); + timer.addAction({ + doAction: () => { + castFn(); + }, + delay: event.delay, + }); + } + } + applyEventsSynchronously(syncEvents); + emitter.emit(ReplayerEvents.Flush); + timer.start(); + }, + pause(ctx) { + ctx.timer.clear(); + }, + resetLastPlayedEvent: o((ctx) => { + return Object.assign(Object.assign({}, ctx), { lastPlayedEvent: null }); + }), + startLive: o({ + baselineTime: (ctx, event) => { + ctx.timer.start(); + if (event.type === 'TO_LIVE' && event.payload.baselineTime) { + return event.payload.baselineTime; + } + return Date.now(); + }, + }), + addEvent: o((ctx, machineEvent) => { + const { baselineTime, timer, events } = ctx; + if (machineEvent.type === 'ADD_EVENT') { + const { event } = machineEvent.payload; + addDelay(event, baselineTime); + let end = events.length - 1; + if (!events[end] || events[end].timestamp <= event.timestamp) { + events.push(event); + } + else { + let insertionIndex = -1; + let start = 0; + while (start <= end) { + const mid = Math.floor((start + end) / 2); + if (events[mid].timestamp <= event.timestamp) { + start = mid + 1; + } + else { + end = mid - 1; + } + } + if (insertionIndex === -1) { + insertionIndex = start; + } + events.splice(insertionIndex, 0, event); + } + const isSync = event.timestamp < baselineTime; + const castFn = getCastFn(event, isSync); + if (isSync) { + castFn(); + } + else if (timer.isActive()) { + timer.addAction({ + doAction: () => { + castFn(); + }, + delay: event.delay, + }); + } + } + return Object.assign(Object.assign({}, ctx), { events }); + }), + }, + }); + return v(playerMachine); +} +function createSpeedService(context) { + const speedMachine = s({ + id: 'speed', + context, + initial: 'normal', + states: { + normal: { + on: { + FAST_FORWARD: { + target: 'skipping', + actions: ['recordSpeed', 'setSpeed'], + }, + SET_SPEED: { + target: 'normal', + actions: ['setSpeed'], + }, + }, + }, + skipping: { + on: { + BACK_TO_NORMAL: { + target: 'normal', + actions: ['restoreSpeed'], + }, + SET_SPEED: { + target: 'normal', + actions: ['setSpeed'], + }, + }, + }, + }, + }, { + actions: { + setSpeed: (ctx, event) => { + if ('payload' in event) { + ctx.timer.setSpeed(event.payload.speed); + } + }, + recordSpeed: o({ + normalSpeed: (ctx) => ctx.timer.speed, + }), + restoreSpeed: (ctx) => { + ctx.timer.setSpeed(ctx.normalSpeed); + }, + }, + }); + return v(speedMachine); +} + +const rules = (blockClass) => [ + `.${blockClass} { background: currentColor }`, + 'noscript { display: none !important; }', +]; + +const webGLVarMap = new Map(); +function variableListFor(ctx, ctor) { + let contextMap = webGLVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + webGLVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); +} +function deserializeArg(imageMap, ctx, preload) { + return (arg) => __awaiter(this, void 0, void 0, function* () { + if (arg && typeof arg === 'object' && 'rr_type' in arg) { + if (preload) + preload.isUnchanged = false; + if (arg.rr_type === 'ImageBitmap' && 'args' in arg) { + const args = yield deserializeArg(imageMap, ctx, preload)(arg.args); + return yield createImageBitmap.apply(null, args); + } + else if ('index' in arg) { + if (preload || ctx === null) + return arg; + const { rr_type: name, index } = arg; + return variableListFor(ctx, name)[index]; + } + else if ('args' in arg) { + const { rr_type: name, args } = arg; + const ctor = window[name]; + return new ctor(...(yield Promise.all(args.map(deserializeArg(imageMap, ctx, preload))))); + } + else if ('base64' in arg) { + return decode(arg.base64); + } + else if ('src' in arg) { + const image = imageMap.get(arg.src); + if (image) { + return image; + } + else { + const image = new Image(); + image.src = arg.src; + imageMap.set(arg.src, image); + return image; + } + } + else if ('data' in arg && arg.rr_type === 'Blob') { + const blobContents = yield Promise.all(arg.data.map(deserializeArg(imageMap, ctx, preload))); + const blob = new Blob(blobContents, { + type: arg.type, + }); + return blob; + } + } + else if (Array.isArray(arg)) { + const result = yield Promise.all(arg.map(deserializeArg(imageMap, ctx, preload))); + return result; + } + return arg; + }); +} + +function getContext(target, type) { + try { + if (type === CanvasContext.WebGL) { + return (target.getContext('webgl') || target.getContext('experimental-webgl')); + } + return target.getContext('webgl2'); + } + catch (e) { + return null; + } +} +const WebGLVariableConstructorsNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', +]; +function saveToWebGLVarMap(ctx, result) { + if (!(result === null || result === void 0 ? void 0 : result.constructor)) + return; + const { name } = result.constructor; + if (!WebGLVariableConstructorsNames.includes(name)) + return; + const variables = variableListFor(ctx, name); + if (!variables.includes(result)) + variables.push(result); +} +function webglMutation({ mutation, target, type, imageMap, errorHandler, }) { + return __awaiter(this, void 0, void 0, function* () { + try { + const ctx = getContext(target, type); + if (!ctx) + return; + if (mutation.setter) { + ctx[mutation.property] = mutation.args[0]; + return; + } + const original = ctx[mutation.property]; + const args = yield Promise.all(mutation.args.map(deserializeArg(imageMap, ctx))); + const result = original.apply(ctx, args); + saveToWebGLVarMap(ctx, result); + const debugMode = false; + if (debugMode) ; + } + catch (error) { + errorHandler(mutation, error); + } + }); +} + +function canvasMutation$1({ event, mutations, target, imageMap, errorHandler, }) { + return __awaiter(this, void 0, void 0, function* () { + const ctx = target.getContext('2d'); + if (!ctx) { + errorHandler(mutations[0], new Error('Canvas context is null')); + return; + } + const mutationArgsPromises = mutations.map((mutation) => __awaiter(this, void 0, void 0, function* () { + return Promise.all(mutation.args.map(deserializeArg(imageMap, ctx))); + })); + const args = yield Promise.all(mutationArgsPromises); + args.forEach((args, index) => { + const mutation = mutations[index]; + try { + if (mutation.setter) { + ctx[mutation.property] = + mutation.args[0]; + return; + } + const original = ctx[mutation.property]; + if (mutation.property === 'drawImage' && + typeof mutation.args[0] === 'string') { + imageMap.get(event); + original.apply(ctx, mutation.args); + } + else { + original.apply(ctx, args); + } + } + catch (error) { + errorHandler(mutation, error); + } + return; + }); + }); +} + +function canvasMutation({ event, mutation, target, imageMap, canvasEventMap, errorHandler, }) { + return __awaiter(this, void 0, void 0, function* () { + try { + const precomputedMutation = canvasEventMap.get(event) || mutation; + const commands = 'commands' in precomputedMutation + ? precomputedMutation.commands + : [precomputedMutation]; + if ([CanvasContext.WebGL, CanvasContext.WebGL2].includes(mutation.type)) { + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + yield webglMutation({ + mutation: command, + type: mutation.type, + target, + imageMap, + errorHandler, + }); + } + return; + } + yield canvasMutation$1({ + event, + mutations: commands, + target, + imageMap, + errorHandler, + }); + } + catch (error) { + errorHandler(mutation, error); + } + }); +} + +class MediaManager { + constructor(options) { + this.mediaMap = new Map(); + this.metadataCallbackMap = new Map(); + this.warn = options.warn; + this.service = options.service; + this.speedService = options.speedService; + this.emitter = options.emitter; + this.getCurrentTime = options.getCurrentTime; + this.emitter.on(ReplayerEvents.Start, this.start.bind(this)); + this.emitter.on(ReplayerEvents.SkipStart, this.start.bind(this)); + this.emitter.on(ReplayerEvents.Pause, this.pause.bind(this)); + this.emitter.on(ReplayerEvents.Finish, this.pause.bind(this)); + this.speedService.subscribe(() => { + this.syncAllMediaElements(); + }); + } + syncAllMediaElements(options = { pause: false }) { + this.mediaMap.forEach((mediaState, target) => { + this.syncTargetWithState(target); + if (options.pause) { + target.pause(); + } + }); + } + start() { + this.syncAllMediaElements(); + } + pause() { + this.syncAllMediaElements({ pause: true }); + } + seekTo({ time, target, mediaState, }) { + if (mediaState.isPlaying) { + const differenceBetweenCurrentTimeAndMediaMutationTimestamp = time - mediaState.lastInteractionTimeOffset; + const mediaPlaybackOffset = (differenceBetweenCurrentTimeAndMediaMutationTimestamp / 1000) * + mediaState.playbackRate; + const duration = 'duration' in target && target.duration; + if (Number.isNaN(duration)) { + this.waitForMetadata(target); + return; + } + let seekToTime = mediaState.currentTimeAtLastInteraction + mediaPlaybackOffset; + if (target.loop && + duration !== false) { + seekToTime = seekToTime % duration; + } + target.currentTime = seekToTime; + } + else { + target.pause(); + target.currentTime = mediaState.currentTimeAtLastInteraction; + } + } + waitForMetadata(target) { + if (this.metadataCallbackMap.has(target)) + return; + if (!('addEventListener' in target)) + return; + const onLoadedMetadata = () => { + this.metadataCallbackMap.delete(target); + const mediaState = this.mediaMap.get(target); + if (!mediaState) + return; + this.seekTo({ + time: this.getCurrentTime(), + target, + mediaState, + }); + }; + this.metadataCallbackMap.set(target, onLoadedMetadata); + target.addEventListener('loadedmetadata', onLoadedMetadata, { + once: true, + }); + } + getMediaStateFromMutation({ target, timeOffset, mutation, }) { + var _a, _b, _c, _d, _e; + const lastState = this.mediaMap.get(target); + const { type, playbackRate, currentTime, muted, volume, loop } = mutation; + const isPlaying = type === 0 || + (type !== 1 && + ((lastState === null || lastState === void 0 ? void 0 : lastState.isPlaying) || target.getAttribute('autoplay') !== null)); + const mediaState = { + isPlaying, + currentTimeAtLastInteraction: (_a = currentTime !== null && currentTime !== void 0 ? currentTime : lastState === null || lastState === void 0 ? void 0 : lastState.currentTimeAtLastInteraction) !== null && _a !== void 0 ? _a : 0, + lastInteractionTimeOffset: timeOffset, + playbackRate: (_b = playbackRate !== null && playbackRate !== void 0 ? playbackRate : lastState === null || lastState === void 0 ? void 0 : lastState.playbackRate) !== null && _b !== void 0 ? _b : 1, + volume: (_c = volume !== null && volume !== void 0 ? volume : lastState === null || lastState === void 0 ? void 0 : lastState.volume) !== null && _c !== void 0 ? _c : 1, + muted: (_d = muted !== null && muted !== void 0 ? muted : lastState === null || lastState === void 0 ? void 0 : lastState.muted) !== null && _d !== void 0 ? _d : target.getAttribute('muted') === null, + loop: (_e = loop !== null && loop !== void 0 ? loop : lastState === null || lastState === void 0 ? void 0 : lastState.loop) !== null && _e !== void 0 ? _e : target.getAttribute('loop') === null, + }; + return mediaState; + } + syncTargetWithState(target) { + const mediaState = this.mediaMap.get(target); + if (!mediaState) + return; + const { muted, loop, volume, isPlaying } = mediaState; + const playerIsPaused = this.service.state.matches('paused'); + const playbackRate = mediaState.playbackRate * this.speedService.state.context.timer.speed; + try { + this.seekTo({ + time: this.getCurrentTime(), + target, + mediaState, + }); + if (target.volume !== volume) { + target.volume = volume; + } + target.muted = muted; + target.loop = loop; + if (target.playbackRate !== playbackRate) { + target.playbackRate = playbackRate; + } + if (isPlaying && !playerIsPaused) { + void target.play(); + } + else { + target.pause(); + } + } + catch (error) { + this.warn(`Failed to replay media interactions: ${error.message || error}`); + } + } + addMediaElements(node, timeOffset, mirror) { + if (!['AUDIO', 'VIDEO'].includes(node.nodeName)) + return; + const target = node; + const serializedNode = mirror.getMeta(target); + if (!serializedNode || !('attributes' in serializedNode)) + return; + const playerIsPaused = this.service.state.matches('paused'); + const mediaAttributes = serializedNode.attributes; + let isPlaying = false; + if (mediaAttributes.rr_mediaState) { + isPlaying = mediaAttributes.rr_mediaState === 'played'; + } + else { + isPlaying = target.getAttribute('autoplay') !== null; + } + if (isPlaying && playerIsPaused) + target.pause(); + let playbackRate = 1; + if (typeof mediaAttributes.rr_mediaPlaybackRate === 'number') { + playbackRate = mediaAttributes.rr_mediaPlaybackRate; + } + let muted = false; + if (typeof mediaAttributes.rr_mediaMuted === 'boolean') { + muted = mediaAttributes.rr_mediaMuted; + } + else { + muted = target.getAttribute('muted') !== null; + } + let loop = false; + if (typeof mediaAttributes.rr_mediaLoop === 'boolean') { + loop = mediaAttributes.rr_mediaLoop; + } + else { + loop = target.getAttribute('loop') !== null; + } + let volume = 1; + if (typeof mediaAttributes.rr_mediaVolume === 'number') { + volume = mediaAttributes.rr_mediaVolume; + } + let currentTimeAtLastInteraction = 0; + if (typeof mediaAttributes.rr_mediaCurrentTime === 'number') { + currentTimeAtLastInteraction = mediaAttributes.rr_mediaCurrentTime; + } + this.mediaMap.set(target, { + isPlaying, + currentTimeAtLastInteraction, + lastInteractionTimeOffset: timeOffset, + playbackRate, + volume, + muted, + loop, + }); + this.syncTargetWithState(target); + } + mediaMutation({ target, timeOffset, mutation, }) { + this.mediaMap.set(target, this.getMediaStateFromMutation({ + target, + timeOffset, + mutation, + })); + this.syncTargetWithState(target); + } + isSupportedMediaElement(node) { + return ['AUDIO', 'VIDEO'].includes(node.nodeName); + } + reset() { + this.mediaMap.clear(); + } +} + +const SKIP_TIME_INTERVAL = 5 * 1000; +const mitt = mitt$1 || mittProxy; +const REPLAY_CONSOLE_PREFIX = '[replayer]'; +const defaultMouseTailConfig = { + duration: 500, + lineCap: 'round', + lineWidth: 3, + strokeStyle: 'red', +}; +function indicatesTouchDevice(e) { + return (e.type == EventType.IncrementalSnapshot && + (e.data.source == IncrementalSource.TouchMove || + (e.data.source == IncrementalSource.MouseInteraction && + e.data.type == MouseInteractions.TouchStart))); +} +class Replayer { + get timer() { + return this.service.state.context.timer; + } + constructor(events, config) { + this.usingVirtualDom = false; + this.virtualDom = new RRDocument(); + this.mouseTail = null; + this.tailPositions = []; + this.emitter = mitt(); + this.legacy_missingNodeRetryMap = {}; + this.cache = createCache(); + this.imageMap = new Map(); + this.canvasEventMap = new Map(); + this.mirror = createMirror$2(); + this.styleMirror = new StyleSheetMirror(); + this.firstFullSnapshot = null; + this.newDocumentQueue = []; + this.mousePos = null; + this.touchActive = null; + this.lastMouseDownEvent = null; + this.lastSelectionData = null; + this.constructedStyleMutations = []; + this.adoptedStyleSheets = []; + this.handleResize = (dimension) => { + this.iframe.style.display = 'inherit'; + for (const el of [this.mouseTail, this.iframe]) { + if (!el) { + continue; + } + el.setAttribute('width', String(dimension.width)); + el.setAttribute('height', String(dimension.height)); + } + }; + this.applyEventsSynchronously = (events) => { + for (const event of events) { + switch (event.type) { + case EventType.DomContentLoaded: + case EventType.Load: + case EventType.Custom: + continue; + case EventType.FullSnapshot: + case EventType.Meta: + case EventType.Plugin: + case EventType.IncrementalSnapshot: + break; + } + const castFn = this.getCastFn(event, true); + castFn(); + } + }; + this.getCastFn = (event, isSync = false) => { + let castFn; + switch (event.type) { + case EventType.DomContentLoaded: + case EventType.Load: + break; + case EventType.Custom: + castFn = () => { + this.emitter.emit(ReplayerEvents.CustomEvent, event); + }; + break; + case EventType.Meta: + castFn = () => this.emitter.emit(ReplayerEvents.Resize, { + width: event.data.width, + height: event.data.height, + }); + break; + case EventType.FullSnapshot: + castFn = () => { + var _a; + if (this.firstFullSnapshot) { + if (this.firstFullSnapshot === event) { + this.firstFullSnapshot = true; + return; + } + } + else { + this.firstFullSnapshot = true; + } + this.mediaManager.reset(); + this.styleMirror.reset(); + this.rebuildFullSnapshot(event, isSync); + (_a = this.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.scrollTo(event.data.initialOffset); + }; + break; + case EventType.IncrementalSnapshot: + castFn = () => { + this.applyIncremental(event, isSync); + if (isSync) { + return; + } + if (event === this.nextUserInteractionEvent) { + this.nextUserInteractionEvent = null; + this.backToNormal(); + } + if (this.config.skipInactive && !this.nextUserInteractionEvent) { + for (const _event of this.service.state.context.events) { + if (_event.timestamp <= event.timestamp) { + continue; + } + if (this.isUserInteraction(_event)) { + if (_event.delay - event.delay > + this.config.inactivePeriodThreshold * + this.speedService.state.context.timer.speed) { + this.nextUserInteractionEvent = _event; + } + break; + } + } + if (this.nextUserInteractionEvent) { + const skipTime = this.nextUserInteractionEvent.delay - event.delay; + const payload = { + speed: Math.min(Math.round(skipTime / SKIP_TIME_INTERVAL), this.config.maxSpeed), + }; + this.speedService.send({ type: 'FAST_FORWARD', payload }); + this.emitter.emit(ReplayerEvents.SkipStart, payload); + } + } + }; + break; + } + const wrappedCastFn = () => { + if (castFn) { + castFn(); + } + for (const plugin of this.config.plugins || []) { + if (plugin.handler) + plugin.handler(event, isSync, { replayer: this }); + } + this.service.send({ type: 'CAST_EVENT', payload: { event } }); + const last_index = this.service.state.context.events.length - 1; + if (!this.config.liveMode && + event === this.service.state.context.events[last_index]) { + const finish = () => { + if (last_index < this.service.state.context.events.length - 1) { + return; + } + this.backToNormal(); + this.service.send('END'); + this.emitter.emit(ReplayerEvents.Finish); + }; + let finish_buffer = 50; + if (event.type === EventType.IncrementalSnapshot && + event.data.source === IncrementalSource.MouseMove && + event.data.positions.length) { + finish_buffer += Math.max(0, -event.data.positions[0].timeOffset); + } + setTimeout(finish, finish_buffer); + } + this.emitter.emit(ReplayerEvents.EventCast, event); + }; + return wrappedCastFn; + }; + if (!(config === null || config === void 0 ? void 0 : config.liveMode) && events.length < 2) { + throw new Error('Replayer need at least 2 events.'); + } + const defaultConfig = { + speed: 1, + maxSpeed: 360, + root: document.body, + loadTimeout: 0, + skipInactive: false, + inactivePeriodThreshold: 10 * 1000, + showWarning: true, + showDebug: false, + blockClass: 'rr-block', + liveMode: false, + insertStyleRules: [], + triggerFocus: true, + UNSAFE_replayCanvas: false, + pauseAnimation: true, + mouseTail: defaultMouseTailConfig, + useVirtualDom: true, + logger: console, + }; + this.config = Object.assign({}, defaultConfig, config); + this.handleResize = this.handleResize.bind(this); + this.getCastFn = this.getCastFn.bind(this); + this.applyEventsSynchronously = this.applyEventsSynchronously.bind(this); + this.emitter.on(ReplayerEvents.Resize, this.handleResize); + this.setupDom(); + for (const plugin of this.config.plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ nodeMirror: this.mirror }); + } + this.emitter.on(ReplayerEvents.Flush, () => { + if (this.usingVirtualDom) { + const replayerHandler = { + mirror: this.mirror, + applyCanvas: (canvasEvent, canvasMutationData, target) => { + void canvasMutation({ + event: canvasEvent, + mutation: canvasMutationData, + target, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + }, + applyInput: this.applyInput.bind(this), + applyScroll: this.applyScroll.bind(this), + applyStyleSheetMutation: (data, styleSheet) => { + if (data.source === IncrementalSource.StyleSheetRule) + this.applyStyleSheetRule(data, styleSheet); + else if (data.source === IncrementalSource.StyleDeclaration) + this.applyStyleDeclaration(data, styleSheet); + }, + afterAppend: (node, id) => { + for (const plugin of this.config.plugins || []) { + if (plugin.onBuild) + plugin.onBuild(node, { id, replayer: this }); + } + }, + }; + if (this.iframe.contentDocument) + try { + diff(this.iframe.contentDocument, this.virtualDom, replayerHandler, this.virtualDom.mirror); + } + catch (e) { + console.warn(e); + } + this.virtualDom.destroyTree(); + this.usingVirtualDom = false; + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + for (const key in this.legacy_missingNodeRetryMap) { + try { + const value = this.legacy_missingNodeRetryMap[key]; + const realNode = createOrGetNode(value.node, this.mirror, this.virtualDom.mirror); + diff(realNode, value.node, replayerHandler, this.virtualDom.mirror); + value.node = realNode; + } + catch (error) { + this.warn(error); + } + } + } + this.constructedStyleMutations.forEach((data) => { + this.applyStyleSheetMutation(data); + }); + this.constructedStyleMutations = []; + this.adoptedStyleSheets.forEach((data) => { + this.applyAdoptedStyleSheet(data); + }); + this.adoptedStyleSheets = []; + } + if (this.mousePos) { + this.moveAndHover(this.mousePos.x, this.mousePos.y, this.mousePos.id, true, this.mousePos.debugData); + this.mousePos = null; + } + if (this.touchActive === true) { + this.mouse.classList.add('touch-active'); + } + else if (this.touchActive === false) { + this.mouse.classList.remove('touch-active'); + } + this.touchActive = null; + if (this.lastMouseDownEvent) { + const [target, event] = this.lastMouseDownEvent; + target.dispatchEvent(event); + } + this.lastMouseDownEvent = null; + if (this.lastSelectionData) { + this.applySelection(this.lastSelectionData); + this.lastSelectionData = null; + } + }); + this.emitter.on(ReplayerEvents.PlayBack, () => { + this.firstFullSnapshot = null; + this.mirror.reset(); + this.styleMirror.reset(); + this.mediaManager.reset(); + }); + const timer = new Timer([], { + speed: this.config.speed, + }); + this.service = createPlayerService({ + events: events + .map((e) => { + if (config && config.unpackFn) { + return config.unpackFn(e); + } + return e; + }) + .sort((a1, a2) => a1.timestamp - a2.timestamp), + timer, + timeOffset: 0, + baselineTime: 0, + lastPlayedEvent: null, + }, { + getCastFn: this.getCastFn, + applyEventsSynchronously: this.applyEventsSynchronously, + emitter: this.emitter, + }); + this.service.start(); + this.service.subscribe((state) => { + this.emitter.emit(ReplayerEvents.StateChange, { + player: state, + }); + }); + this.speedService = createSpeedService({ + normalSpeed: -1, + timer, + }); + this.speedService.start(); + this.speedService.subscribe((state) => { + this.emitter.emit(ReplayerEvents.StateChange, { + speed: state, + }); + }); + this.mediaManager = new MediaManager({ + warn: this.warn.bind(this), + service: this.service, + speedService: this.speedService, + emitter: this.emitter, + getCurrentTime: this.getCurrentTime.bind(this), + }); + const firstMeta = this.service.state.context.events.find((e) => e.type === EventType.Meta); + const firstFullsnapshot = this.service.state.context.events.find((e) => e.type === EventType.FullSnapshot); + if (firstMeta) { + const { width, height } = firstMeta.data; + setTimeout(() => { + this.emitter.emit(ReplayerEvents.Resize, { + width, + height, + }); + }, 0); + } + if (firstFullsnapshot) { + setTimeout(() => { + var _a; + if (this.firstFullSnapshot) { + return; + } + this.firstFullSnapshot = firstFullsnapshot; + this.rebuildFullSnapshot(firstFullsnapshot); + (_a = this.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.scrollTo(firstFullsnapshot.data.initialOffset); + }, 1); + } + if (this.service.state.context.events.find(indicatesTouchDevice)) { + this.mouse.classList.add('touch-device'); + } + } + on(event, handler) { + this.emitter.on(event, handler); + return this; + } + off(event, handler) { + this.emitter.off(event, handler); + return this; + } + setConfig(config) { + Object.keys(config).forEach((key) => { + config[key]; + this.config[key] = config[key]; + }); + if (!this.config.skipInactive) { + this.backToNormal(); + } + if (typeof config.speed !== 'undefined') { + this.speedService.send({ + type: 'SET_SPEED', + payload: { + speed: config.speed, + }, + }); + } + if (typeof config.mouseTail !== 'undefined') { + if (config.mouseTail === false) { + if (this.mouseTail) { + this.mouseTail.style.display = 'none'; + } + } + else { + if (!this.mouseTail) { + this.mouseTail = document.createElement('canvas'); + this.mouseTail.width = Number.parseFloat(this.iframe.width); + this.mouseTail.height = Number.parseFloat(this.iframe.height); + this.mouseTail.classList.add('replayer-mouse-tail'); + this.wrapper.insertBefore(this.mouseTail, this.iframe); + } + this.mouseTail.style.display = 'inherit'; + } + } + } + getMetaData() { + const firstEvent = this.service.state.context.events[0]; + const lastEvent = this.service.state.context.events[this.service.state.context.events.length - 1]; + return { + startTime: firstEvent.timestamp, + endTime: lastEvent.timestamp, + totalTime: lastEvent.timestamp - firstEvent.timestamp, + }; + } + getCurrentTime() { + return this.timer.timeOffset + this.getTimeOffset(); + } + getTimeOffset() { + const { baselineTime, events } = this.service.state.context; + return baselineTime - events[0].timestamp; + } + getMirror() { + return this.mirror; + } + play(timeOffset = 0) { + var _a, _b; + if (this.service.state.matches('paused')) { + this.service.send({ type: 'PLAY', payload: { timeOffset } }); + } + else { + this.service.send({ type: 'PAUSE' }); + this.service.send({ type: 'PLAY', payload: { timeOffset } }); + } + (_b = (_a = this.iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.getElementsByTagName('html')[0]) === null || _b === void 0 ? void 0 : _b.classList.remove('rrweb-paused'); + this.emitter.emit(ReplayerEvents.Start); + } + pause(timeOffset) { + var _a, _b; + if (timeOffset === undefined && this.service.state.matches('playing')) { + this.service.send({ type: 'PAUSE' }); + } + if (typeof timeOffset === 'number') { + this.play(timeOffset); + this.service.send({ type: 'PAUSE' }); + } + (_b = (_a = this.iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.getElementsByTagName('html')[0]) === null || _b === void 0 ? void 0 : _b.classList.add('rrweb-paused'); + this.emitter.emit(ReplayerEvents.Pause); + } + resume(timeOffset = 0) { + this.warn(`The 'resume' was deprecated in 1.0. Please use 'play' method which has the same interface.`); + this.play(timeOffset); + this.emitter.emit(ReplayerEvents.Resume); + } + destroy() { + this.pause(); + this.mirror.reset(); + this.styleMirror.reset(); + this.mediaManager.reset(); + this.config.root.removeChild(this.wrapper); + this.emitter.emit(ReplayerEvents.Destroy); + } + startLive(baselineTime) { + this.service.send({ type: 'TO_LIVE', payload: { baselineTime } }); + } + addEvent(rawEvent) { + const event = this.config.unpackFn + ? this.config.unpackFn(rawEvent) + : rawEvent; + if (indicatesTouchDevice(event)) { + this.mouse.classList.add('touch-device'); + } + void Promise.resolve().then(() => this.service.send({ type: 'ADD_EVENT', payload: { event } })); + } + enableInteract() { + this.iframe.setAttribute('scrolling', 'auto'); + this.iframe.style.pointerEvents = 'auto'; + } + disableInteract() { + this.iframe.setAttribute('scrolling', 'no'); + this.iframe.style.pointerEvents = 'none'; + } + resetCache() { + this.cache = createCache(); + } + setupDom() { + this.wrapper = document.createElement('div'); + this.wrapper.classList.add('replayer-wrapper'); + this.config.root.appendChild(this.wrapper); + this.mouse = document.createElement('div'); + this.mouse.classList.add('replayer-mouse'); + this.wrapper.appendChild(this.mouse); + if (this.config.mouseTail !== false) { + this.mouseTail = document.createElement('canvas'); + this.mouseTail.classList.add('replayer-mouse-tail'); + this.mouseTail.style.display = 'inherit'; + this.wrapper.appendChild(this.mouseTail); + } + this.iframe = document.createElement('iframe'); + const attributes = ['allow-same-origin']; + if (this.config.UNSAFE_replayCanvas) { + attributes.push('allow-scripts'); + } + this.iframe.style.display = 'none'; + this.iframe.setAttribute('sandbox', attributes.join(' ')); + this.disableInteract(); + this.wrapper.appendChild(this.iframe); + if (this.iframe.contentWindow && this.iframe.contentDocument) { + polyfill(this.iframe.contentWindow, this.iframe.contentDocument); + polyfill$1(this.iframe.contentWindow); + } + } + rebuildFullSnapshot(event, isSync = false) { + if (!this.iframe.contentDocument) { + return this.warn('Looks like your replayer has been destroyed.'); + } + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + this.warn('Found unresolved missing node map', this.legacy_missingNodeRetryMap); + } + this.legacy_missingNodeRetryMap = {}; + const collected = []; + const afterAppend = (builtNode, id) => { + this.collectIframeAndAttachDocument(collected, builtNode); + if (this.mediaManager.isSupportedMediaElement(builtNode)) { + const { events } = this.service.state.context; + this.mediaManager.addMediaElements(builtNode, event.timestamp - events[0].timestamp, this.mirror); + } + for (const plugin of this.config.plugins || []) { + if (plugin.onBuild) + plugin.onBuild(builtNode, { + id, + replayer: this, + }); + } + }; + if (this.usingVirtualDom) { + this.virtualDom.destroyTree(); + this.usingVirtualDom = false; + } + this.mirror.reset(); + rebuild(event.data.node, { + doc: this.iframe.contentDocument, + afterAppend, + cache: this.cache, + mirror: this.mirror, + }); + afterAppend(this.iframe.contentDocument, event.data.node.id); + for (const { mutationInQueue, builtNode } of collected) { + this.attachDocumentToIframe(mutationInQueue, builtNode); + this.newDocumentQueue = this.newDocumentQueue.filter((m) => m !== mutationInQueue); + } + const { documentElement, head } = this.iframe.contentDocument; + this.insertStyleRules(documentElement, head); + if (!this.service.state.matches('playing')) { + this.iframe.contentDocument + .getElementsByTagName('html')[0] + .classList.add('rrweb-paused'); + } + this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event); + if (!isSync) { + this.waitForStylesheetLoad(); + } + if (this.config.UNSAFE_replayCanvas) { + void this.preloadAllImages(); + } + } + insertStyleRules(documentElement, head) { + var _a; + const injectStylesRules = rules(this.config.blockClass).concat(this.config.insertStyleRules); + if (this.config.pauseAnimation) { + injectStylesRules.push('html.rrweb-paused *, html.rrweb-paused *:before, html.rrweb-paused *:after { animation-play-state: paused !important; }'); + } + if (this.usingVirtualDom) { + const styleEl = this.virtualDom.createElement('style'); + this.virtualDom.mirror.add(styleEl, getDefaultSN(styleEl, this.virtualDom.unserializedId)); + documentElement.insertBefore(styleEl, head); + styleEl.rules.push({ + source: IncrementalSource.StyleSheetRule, + adds: injectStylesRules.map((cssText, index) => ({ + rule: cssText, + index, + })), + }); + } + else { + const styleEl = document.createElement('style'); + documentElement.insertBefore(styleEl, head); + for (let idx = 0; idx < injectStylesRules.length; idx++) { + (_a = styleEl.sheet) === null || _a === void 0 ? void 0 : _a.insertRule(injectStylesRules[idx], idx); + } + } + } + attachDocumentToIframe(mutation, iframeEl) { + const mirror = this.usingVirtualDom + ? this.virtualDom.mirror + : this.mirror; + const collected = []; + const afterAppend = (builtNode, id) => { + this.collectIframeAndAttachDocument(collected, builtNode); + const sn = mirror.getMeta(builtNode); + if ((sn === null || sn === void 0 ? void 0 : sn.type) === NodeType$2.Element && + (sn === null || sn === void 0 ? void 0 : sn.tagName.toUpperCase()) === 'HTML') { + const { documentElement, head } = iframeEl.contentDocument; + this.insertStyleRules(documentElement, head); + } + if (this.usingVirtualDom) + return; + for (const plugin of this.config.plugins || []) { + if (plugin.onBuild) + plugin.onBuild(builtNode, { + id, + replayer: this, + }); + } + }; + buildNodeWithSN(mutation.node, { + doc: iframeEl.contentDocument, + mirror: mirror, + hackCss: true, + skipChild: false, + afterAppend, + cache: this.cache, + }); + afterAppend(iframeEl.contentDocument, mutation.node.id); + for (const { mutationInQueue, builtNode } of collected) { + this.attachDocumentToIframe(mutationInQueue, builtNode); + this.newDocumentQueue = this.newDocumentQueue.filter((m) => m !== mutationInQueue); + } + } + collectIframeAndAttachDocument(collected, builtNode) { + if (isSerializedIframe(builtNode, this.mirror)) { + const mutationInQueue = this.newDocumentQueue.find((m) => m.parentId === this.mirror.getId(builtNode)); + if (mutationInQueue) { + collected.push({ + mutationInQueue, + builtNode: builtNode, + }); + } + } + } + waitForStylesheetLoad() { + var _a; + const head = (_a = this.iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.head; + if (head) { + const unloadSheets = new Set(); + let timer; + let beforeLoadState = this.service.state; + const stateHandler = () => { + beforeLoadState = this.service.state; + }; + this.emitter.on(ReplayerEvents.Start, stateHandler); + this.emitter.on(ReplayerEvents.Pause, stateHandler); + const unsubscribe = () => { + this.emitter.off(ReplayerEvents.Start, stateHandler); + this.emitter.off(ReplayerEvents.Pause, stateHandler); + }; + head + .querySelectorAll('link[rel="stylesheet"]') + .forEach((css) => { + if (!css.sheet) { + unloadSheets.add(css); + css.addEventListener('load', () => { + unloadSheets.delete(css); + if (unloadSheets.size === 0 && timer !== -1) { + if (beforeLoadState.matches('playing')) { + this.play(this.getCurrentTime()); + } + this.emitter.emit(ReplayerEvents.LoadStylesheetEnd); + if (timer) { + clearTimeout(timer); + } + unsubscribe(); + } + }); + } + }); + if (unloadSheets.size > 0) { + this.service.send({ type: 'PAUSE' }); + this.emitter.emit(ReplayerEvents.LoadStylesheetStart); + timer = setTimeout(() => { + if (beforeLoadState.matches('playing')) { + this.play(this.getCurrentTime()); + } + timer = -1; + unsubscribe(); + }, this.config.loadTimeout); + } + } + } + preloadAllImages() { + return __awaiter(this, void 0, void 0, function* () { + this.service.state; + const stateHandler = () => { + this.service.state; + }; + this.emitter.on(ReplayerEvents.Start, stateHandler); + this.emitter.on(ReplayerEvents.Pause, stateHandler); + const promises = []; + for (const event of this.service.state.context.events) { + if (event.type === EventType.IncrementalSnapshot && + event.data.source === IncrementalSource.CanvasMutation) { + promises.push(this.deserializeAndPreloadCanvasEvents(event.data, event)); + const commands = 'commands' in event.data ? event.data.commands : [event.data]; + commands.forEach((c) => { + this.preloadImages(c, event); + }); + } + } + return Promise.all(promises); + }); + } + preloadImages(data, event) { + if (data.property === 'drawImage' && + typeof data.args[0] === 'string' && + !this.imageMap.has(event)) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const imgd = ctx === null || ctx === void 0 ? void 0 : ctx.createImageData(canvas.width, canvas.height); + imgd === null || imgd === void 0 ? void 0 : imgd.data; + JSON.parse(data.args[0]); + ctx === null || ctx === void 0 ? void 0 : ctx.putImageData(imgd, 0, 0); + } + } + deserializeAndPreloadCanvasEvents(data, event) { + return __awaiter(this, void 0, void 0, function* () { + if (!this.canvasEventMap.has(event)) { + const status = { + isUnchanged: true, + }; + if ('commands' in data) { + const commands = yield Promise.all(data.commands.map((c) => __awaiter(this, void 0, void 0, function* () { + const args = yield Promise.all(c.args.map(deserializeArg(this.imageMap, null, status))); + return Object.assign(Object.assign({}, c), { args }); + }))); + if (status.isUnchanged === false) + this.canvasEventMap.set(event, Object.assign(Object.assign({}, data), { commands })); + } + else { + const args = yield Promise.all(data.args.map(deserializeArg(this.imageMap, null, status))); + if (status.isUnchanged === false) + this.canvasEventMap.set(event, Object.assign(Object.assign({}, data), { args })); + } + } + }); + } + applyIncremental(e, isSync) { + var _a, _b, _c; + const { data: d } = e; + switch (d.source) { + case IncrementalSource.Mutation: { + try { + this.applyMutation(d, isSync); + } + catch (error) { + this.warn(`Exception in mutation ${error.message || error}`, d); + } + break; + } + case IncrementalSource.Drag: + case IncrementalSource.TouchMove: + case IncrementalSource.MouseMove: + if (isSync) { + const lastPosition = d.positions[d.positions.length - 1]; + this.mousePos = { + x: lastPosition.x, + y: lastPosition.y, + id: lastPosition.id, + debugData: d, + }; + } + else { + d.positions.forEach((p) => { + const action = { + doAction: () => { + this.moveAndHover(p.x, p.y, p.id, isSync, d); + }, + delay: p.timeOffset + + e.timestamp - + this.service.state.context.baselineTime, + }; + this.timer.addAction(action); + }); + this.timer.addAction({ + doAction() { + }, + delay: e.delay - ((_a = d.positions[0]) === null || _a === void 0 ? void 0 : _a.timeOffset), + }); + } + break; + case IncrementalSource.MouseInteraction: { + if (d.id === -1) { + break; + } + const event = new Event(toLowerCase(MouseInteractions[d.type])); + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + this.emitter.emit(ReplayerEvents.MouseInteraction, { + type: d.type, + target, + }); + const { triggerFocus } = this.config; + switch (d.type) { + case MouseInteractions.Blur: + if ('blur' in target) { + target.blur(); + } + break; + case MouseInteractions.Focus: + if (triggerFocus && target.focus) { + target.focus({ + preventScroll: true, + }); + } + break; + case MouseInteractions.Click: + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + if (isSync) { + if (d.type === MouseInteractions.TouchStart) { + this.touchActive = true; + } + else if (d.type === MouseInteractions.TouchEnd) { + this.touchActive = false; + } + if (d.type === MouseInteractions.MouseDown) { + this.lastMouseDownEvent = [target, event]; + } + else if (d.type === MouseInteractions.MouseUp) { + this.lastMouseDownEvent = null; + } + this.mousePos = { + x: d.x || 0, + y: d.y || 0, + id: d.id, + debugData: d, + }; + } + else { + if (d.type === MouseInteractions.TouchStart) { + this.tailPositions.length = 0; + } + this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d); + if (d.type === MouseInteractions.Click) { + this.mouse.classList.remove('active'); + void this.mouse.offsetWidth; + this.mouse.classList.add('active'); + } + else if (d.type === MouseInteractions.TouchStart) { + void this.mouse.offsetWidth; + this.mouse.classList.add('touch-active'); + } + else if (d.type === MouseInteractions.TouchEnd) { + this.mouse.classList.remove('touch-active'); + } + else { + target.dispatchEvent(event); + } + } + break; + case MouseInteractions.TouchCancel: + if (isSync) { + this.touchActive = false; + } + else { + this.mouse.classList.remove('touch-active'); + } + break; + default: + target.dispatchEvent(event); + } + break; + } + case IncrementalSource.Scroll: { + if (d.id === -1) { + break; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.scrollData = d; + break; + } + this.applyScroll(d, isSync); + break; + } + case IncrementalSource.ViewportResize: + this.emitter.emit(ReplayerEvents.Resize, { + width: d.width, + height: d.height, + }); + break; + case IncrementalSource.Input: { + if (d.id === -1) { + break; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.inputData = d; + break; + } + this.applyInput(d); + break; + } + case IncrementalSource.MediaInteraction: { + const target = this.usingVirtualDom + ? this.virtualDom.mirror.getNode(d.id) + : this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const mediaEl = target; + const { events } = this.service.state.context; + this.mediaManager.mediaMutation({ + target: mediaEl, + timeOffset: e.timestamp - events[0].timestamp, + mutation: d, + }); + break; + } + case IncrementalSource.StyleSheetRule: + case IncrementalSource.StyleDeclaration: { + if (this.usingVirtualDom) { + if (d.styleId) + this.constructedStyleMutations.push(d); + else if (d.id) + (_b = this.virtualDom.mirror.getNode(d.id)) === null || _b === void 0 ? void 0 : _b.rules.push(d); + } + else + this.applyStyleSheetMutation(d); + break; + } + case IncrementalSource.CanvasMutation: { + if (!this.config.UNSAFE_replayCanvas) { + return; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.canvasMutations.push({ + event: e, + mutation: d, + }); + } + else { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + void canvasMutation({ + event: e, + mutation: d, + target: target, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + } + break; + } + case IncrementalSource.Font: { + try { + const fontFace = new FontFace(d.family, d.buffer + ? new Uint8Array(JSON.parse(d.fontSource)) + : d.fontSource, d.descriptors); + (_c = this.iframe.contentDocument) === null || _c === void 0 ? void 0 : _c.fonts.add(fontFace); + } + catch (error) { + this.warn(error); + } + break; + } + case IncrementalSource.Selection: { + if (isSync) { + this.lastSelectionData = d; + break; + } + this.applySelection(d); + break; + } + case IncrementalSource.AdoptedStyleSheet: { + if (this.usingVirtualDom) + this.adoptedStyleSheets.push(d); + else + this.applyAdoptedStyleSheet(d); + break; + } + } + } + applyMutation(d, isSync) { + if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) { + this.usingVirtualDom = true; + buildFromDom(this.iframe.contentDocument, this.mirror, this.virtualDom); + if (Object.keys(this.legacy_missingNodeRetryMap).length) { + for (const key in this.legacy_missingNodeRetryMap) { + try { + const value = this.legacy_missingNodeRetryMap[key]; + const virtualNode = buildFromNode(value.node, this.virtualDom, this.mirror); + if (virtualNode) + value.node = virtualNode; + } + catch (error) { + this.warn(error); + } + } + } + } + const mirror = this.usingVirtualDom ? this.virtualDom.mirror : this.mirror; + d.removes = d.removes.filter((mutation) => { + if (!mirror.getNode(mutation.id)) { + this.warnNodeNotFound(d, mutation.id); + return false; + } + return true; + }); + d.removes.forEach((mutation) => { + var _a; + const target = mirror.getNode(mutation.id); + if (!target) { + return; + } + let parent = mirror.getNode(mutation.parentId); + if (!parent) { + return this.warnNodeNotFound(d, mutation.parentId); + } + if (mutation.isShadow && hasShadowRoot(parent)) { + parent = parent.shadowRoot; + } + mirror.removeNodeFromMap(target); + if (parent) + try { + parent.removeChild(target); + if (this.usingVirtualDom && + target.nodeName === '#text' && + parent.nodeName === 'STYLE' && + ((_a = parent.rules) === null || _a === void 0 ? void 0 : _a.length) > 0) + parent.rules = []; + } + catch (error) { + if (error instanceof DOMException) { + this.warn('parent could not remove child in mutation', parent, target, d); + } + else { + throw error; + } + } + }); + const legacy_missingNodeMap = Object.assign({}, this.legacy_missingNodeRetryMap); + const queue = []; + const nextNotInDOM = (mutation) => { + let next = null; + if (mutation.nextId) { + next = mirror.getNode(mutation.nextId); + } + if (mutation.nextId !== null && + mutation.nextId !== undefined && + mutation.nextId !== -1 && + !next) { + return true; + } + return false; + }; + const appendNode = (mutation) => { + var _a, _b; + if (!this.iframe.contentDocument) { + return this.warn('Looks like your replayer has been destroyed.'); + } + let parent = mirror.getNode(mutation.parentId); + if (!parent) { + if (mutation.node.type === NodeType$2.Document) { + return this.newDocumentQueue.push(mutation); + } + return queue.push(mutation); + } + if (mutation.node.isShadow) { + if (!hasShadowRoot(parent)) { + parent.attachShadow({ mode: 'open' }); + parent = parent.shadowRoot; + } + else + parent = parent.shadowRoot; + } + let previous = null; + let next = null; + if (mutation.previousId) { + previous = mirror.getNode(mutation.previousId); + } + if (mutation.nextId) { + next = mirror.getNode(mutation.nextId); + } + if (nextNotInDOM(mutation)) { + return queue.push(mutation); + } + if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) { + return; + } + const targetDoc = mutation.node.rootId + ? mirror.getNode(mutation.node.rootId) + : this.usingVirtualDom + ? this.virtualDom + : this.iframe.contentDocument; + if (isSerializedIframe(parent, mirror)) { + this.attachDocumentToIframe(mutation, parent); + return; + } + const afterAppend = (node, id) => { + if (this.usingVirtualDom) + return; + for (const plugin of this.config.plugins || []) { + if (plugin.onBuild) + plugin.onBuild(node, { id, replayer: this }); + } + }; + const target = buildNodeWithSN(mutation.node, { + doc: targetDoc, + mirror: mirror, + skipChild: true, + hackCss: true, + cache: this.cache, + afterAppend, + }); + if (mutation.previousId === -1 || mutation.nextId === -1) { + legacy_missingNodeMap[mutation.node.id] = { + node: target, + mutation, + }; + return; + } + const parentSn = mirror.getMeta(parent); + if (parentSn && + parentSn.type === NodeType$2.Element && + parentSn.tagName === 'textarea' && + mutation.node.type === NodeType$2.Text) { + const childNodeArray = Array.isArray(parent.childNodes) + ? parent.childNodes + : Array.from(parent.childNodes); + for (const c of childNodeArray) { + if (c.nodeType === parent.TEXT_NODE) { + parent.removeChild(c); + } + } + } + else if ((parentSn === null || parentSn === void 0 ? void 0 : parentSn.type) === NodeType$2.Document) { + const parentDoc = parent; + if (mutation.node.type === NodeType$2.DocumentType && + ((_a = parentDoc.childNodes[0]) === null || _a === void 0 ? void 0 : _a.nodeType) === Node.DOCUMENT_TYPE_NODE) + parentDoc.removeChild(parentDoc.childNodes[0]); + if (target.nodeName === 'HTML' && parentDoc.documentElement) + parentDoc.removeChild(parentDoc.documentElement); + } + if (previous && previous.nextSibling && previous.nextSibling.parentNode) { + parent.insertBefore(target, previous.nextSibling); + } + else if (next && next.parentNode) { + parent.contains(next) + ? parent.insertBefore(target, next) + : parent.insertBefore(target, null); + } + else { + parent.appendChild(target); + } + afterAppend(target, mutation.node.id); + if (this.usingVirtualDom && + target.nodeName === '#text' && + parent.nodeName === 'STYLE' && + ((_b = parent.rules) === null || _b === void 0 ? void 0 : _b.length) > 0) + parent.rules = []; + if (isSerializedIframe(target, this.mirror)) { + const targetId = this.mirror.getId(target); + const mutationInQueue = this.newDocumentQueue.find((m) => m.parentId === targetId); + if (mutationInQueue) { + this.attachDocumentToIframe(mutationInQueue, target); + this.newDocumentQueue = this.newDocumentQueue.filter((m) => m !== mutationInQueue); + } + } + if (mutation.previousId || mutation.nextId) { + this.legacy_resolveMissingNode(legacy_missingNodeMap, parent, target, mutation); + } + }; + d.adds.forEach((mutation) => { + appendNode(mutation); + }); + const startTime = Date.now(); + while (queue.length) { + const resolveTrees = queueToResolveTrees(queue); + queue.length = 0; + if (Date.now() - startTime > 500) { + this.warn('Timeout in the loop, please check the resolve tree data:', resolveTrees); + break; + } + for (const tree of resolveTrees) { + const parent = mirror.getNode(tree.value.parentId); + if (!parent) { + this.debug('Drop resolve tree since there is no parent for the root node.', tree); + } + else { + iterateResolveTree(tree, (mutation) => { + appendNode(mutation); + }); + } + } + } + if (Object.keys(legacy_missingNodeMap).length) { + Object.assign(this.legacy_missingNodeRetryMap, legacy_missingNodeMap); + } + uniqueTextMutations(d.texts).forEach((mutation) => { + var _a; + const target = mirror.getNode(mutation.id); + if (!target) { + if (d.removes.find((r) => r.id === mutation.id)) { + return; + } + return this.warnNodeNotFound(d, mutation.id); + } + target.textContent = mutation.value; + if (this.usingVirtualDom) { + const parent = target.parentNode; + if (((_a = parent === null || parent === void 0 ? void 0 : parent.rules) === null || _a === void 0 ? void 0 : _a.length) > 0) + parent.rules = []; + } + }); + d.attributes.forEach((mutation) => { + var _a; + const target = mirror.getNode(mutation.id); + if (!target) { + if (d.removes.find((r) => r.id === mutation.id)) { + return; + } + return this.warnNodeNotFound(d, mutation.id); + } + for (const attributeName in mutation.attributes) { + if (typeof attributeName === 'string') { + const value = mutation.attributes[attributeName]; + if (value === null) { + target.removeAttribute(attributeName); + } + else if (typeof value === 'string') { + try { + if (attributeName === '_cssText' && + (target.nodeName === 'LINK' || target.nodeName === 'STYLE')) { + try { + const newSn = mirror.getMeta(target); + Object.assign(newSn.attributes, mutation.attributes); + const newNode = buildNodeWithSN(newSn, { + doc: target.ownerDocument, + mirror: mirror, + skipChild: true, + hackCss: true, + cache: this.cache, + }); + const siblingNode = target.nextSibling; + const parentNode = target.parentNode; + if (newNode && parentNode) { + parentNode.removeChild(target); + parentNode.insertBefore(newNode, siblingNode); + mirror.replace(mutation.id, newNode); + break; + } + } + catch (e) { + } + } + if (attributeName === 'value' && target.nodeName === 'TEXTAREA') { + const textarea = target; + textarea.childNodes.forEach((c) => textarea.removeChild(c)); + const tn = (_a = target.ownerDocument) === null || _a === void 0 ? void 0 : _a.createTextNode(value); + if (tn) { + textarea.appendChild(tn); + } + } + else { + target.setAttribute(attributeName, value); + } + } + catch (error) { + this.warn('An error occurred may due to the checkout feature.', error); + } + } + else if (attributeName === 'style') { + const styleValues = value; + const targetEl = target; + for (const s in styleValues) { + if (styleValues[s] === false) { + targetEl.style.removeProperty(s); + } + else if (styleValues[s] instanceof Array) { + const svp = styleValues[s]; + targetEl.style.setProperty(s, svp[0], svp[1]); + } + else { + const svs = styleValues[s]; + targetEl.style.setProperty(s, svs); + } + } + } + } + } + }); + } + applyScroll(d, isSync) { + var _a, _b; + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + const sn = this.mirror.getMeta(target); + if (target === this.iframe.contentDocument) { + (_a = this.iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.scrollTo({ + top: d.y, + left: d.x, + behavior: isSync ? 'auto' : 'smooth', + }); + } + else if ((sn === null || sn === void 0 ? void 0 : sn.type) === NodeType$2.Document) { + (_b = target.defaultView) === null || _b === void 0 ? void 0 : _b.scrollTo({ + top: d.y, + left: d.x, + behavior: isSync ? 'auto' : 'smooth', + }); + } + else { + try { + target.scrollTo({ + top: d.y, + left: d.x, + behavior: isSync ? 'auto' : 'smooth', + }); + } + catch (error) { + } + } + } + applyInput(d) { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + try { + target.checked = d.isChecked; + target.value = d.text; + } + catch (error) { + } + } + applySelection(d) { + try { + const selectionSet = new Set(); + const ranges = d.ranges.map(({ start, startOffset, end, endOffset }) => { + const startContainer = this.mirror.getNode(start); + const endContainer = this.mirror.getNode(end); + if (!startContainer || !endContainer) + return; + const result = new Range(); + result.setStart(startContainer, startOffset); + result.setEnd(endContainer, endOffset); + const doc = startContainer.ownerDocument; + const selection = doc === null || doc === void 0 ? void 0 : doc.getSelection(); + selection && selectionSet.add(selection); + return { + range: result, + selection, + }; + }); + selectionSet.forEach((s) => s.removeAllRanges()); + ranges.forEach((r) => { var _a; return r && ((_a = r.selection) === null || _a === void 0 ? void 0 : _a.addRange(r.range)); }); + } + catch (error) { + } + } + applyStyleSheetMutation(data) { + var _a; + let styleSheet = null; + if (data.styleId) + styleSheet = this.styleMirror.getStyle(data.styleId); + else if (data.id) + styleSheet = + ((_a = this.mirror.getNode(data.id)) === null || _a === void 0 ? void 0 : _a.sheet) || null; + if (!styleSheet) + return; + if (data.source === IncrementalSource.StyleSheetRule) + this.applyStyleSheetRule(data, styleSheet); + else if (data.source === IncrementalSource.StyleDeclaration) + this.applyStyleDeclaration(data, styleSheet); + } + applyStyleSheetRule(data, styleSheet) { + var _a, _b, _c, _d; + (_a = data.adds) === null || _a === void 0 ? void 0 : _a.forEach(({ rule, index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule(styleSheet.cssRules, positions); + nestedRule.insertRule(rule, index); + } + else { + const index = nestedIndex === undefined + ? undefined + : Math.min(nestedIndex, styleSheet.cssRules.length); + styleSheet === null || styleSheet === void 0 ? void 0 : styleSheet.insertRule(rule, index); + } + } + catch (e) { + } + }); + (_b = data.removes) === null || _b === void 0 ? void 0 : _b.forEach(({ index: nestedIndex }) => { + try { + if (Array.isArray(nestedIndex)) { + const { positions, index } = getPositionsAndIndex(nestedIndex); + const nestedRule = getNestedRule(styleSheet.cssRules, positions); + nestedRule.deleteRule(index || 0); + } + else { + styleSheet === null || styleSheet === void 0 ? void 0 : styleSheet.deleteRule(nestedIndex); + } + } + catch (e) { + } + }); + if (data.replace) + try { + void ((_c = styleSheet.replace) === null || _c === void 0 ? void 0 : _c.call(styleSheet, data.replace)); + } + catch (e) { + } + if (data.replaceSync) + try { + (_d = styleSheet.replaceSync) === null || _d === void 0 ? void 0 : _d.call(styleSheet, data.replaceSync); + } + catch (e) { + } + } + applyStyleDeclaration(data, styleSheet) { + if (data.set) { + const rule = getNestedRule(styleSheet.rules, data.index); + rule.style.setProperty(data.set.property, data.set.value, data.set.priority); + } + if (data.remove) { + const rule = getNestedRule(styleSheet.rules, data.index); + rule.style.removeProperty(data.remove.property); + } + } + applyAdoptedStyleSheet(data) { + var _a; + const targetHost = this.mirror.getNode(data.id); + if (!targetHost) + return; + (_a = data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + var _a; + let newStyleSheet = null; + let hostWindow = null; + if (hasShadowRoot(targetHost)) + hostWindow = ((_a = targetHost.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView) || null; + else if (targetHost.nodeName === '#document') + hostWindow = targetHost.defaultView; + if (!hostWindow) + return; + try { + newStyleSheet = new hostWindow.CSSStyleSheet(); + this.styleMirror.add(newStyleSheet, style.styleId); + this.applyStyleSheetRule({ + source: IncrementalSource.StyleSheetRule, + adds: style.rules, + }, newStyleSheet); + } + catch (e) { + } + }); + const MAX_RETRY_TIME = 10; + let count = 0; + const adoptStyleSheets = (targetHost, styleIds) => { + const stylesToAdopt = styleIds + .map((styleId) => this.styleMirror.getStyle(styleId)) + .filter((style) => style !== null); + if (hasShadowRoot(targetHost)) + targetHost.shadowRoot.adoptedStyleSheets = + stylesToAdopt; + else if (targetHost.nodeName === '#document') + targetHost.adoptedStyleSheets = stylesToAdopt; + if (stylesToAdopt.length !== styleIds.length && count < MAX_RETRY_TIME) { + setTimeout(() => adoptStyleSheets(targetHost, styleIds), 0 + 100 * count); + count++; + } + }; + adoptStyleSheets(targetHost, data.styleIds); + } + legacy_resolveMissingNode(map, parent, target, targetMutation) { + const { previousId, nextId } = targetMutation; + const previousInMap = previousId && map[previousId]; + const nextInMap = nextId && map[nextId]; + if (previousInMap) { + const { node, mutation } = previousInMap; + parent.insertBefore(node, target); + delete map[mutation.node.id]; + delete this.legacy_missingNodeRetryMap[mutation.node.id]; + if (mutation.previousId || mutation.nextId) { + this.legacy_resolveMissingNode(map, parent, node, mutation); + } + } + if (nextInMap) { + const { node, mutation } = nextInMap; + parent.insertBefore(node, target.nextSibling); + delete map[mutation.node.id]; + delete this.legacy_missingNodeRetryMap[mutation.node.id]; + if (mutation.previousId || mutation.nextId) { + this.legacy_resolveMissingNode(map, parent, node, mutation); + } + } + } + moveAndHover(x, y, id, isSync, debugData) { + const target = this.mirror.getNode(id); + if (!target) { + return this.debugNodeNotFound(debugData, id); + } + const base = getBaseDimension(target, this.iframe); + const _x = x * base.absoluteScale + base.x; + const _y = y * base.absoluteScale + base.y; + this.mouse.style.left = `${_x}px`; + this.mouse.style.top = `${_y}px`; + if (!isSync) { + this.drawMouseTail({ x: _x, y: _y }); + } + this.hoverElements(target); + } + drawMouseTail(position) { + if (!this.mouseTail) { + return; + } + const { lineCap, lineWidth, strokeStyle, duration } = this.config.mouseTail === true + ? defaultMouseTailConfig + : Object.assign({}, defaultMouseTailConfig, this.config.mouseTail); + const draw = () => { + if (!this.mouseTail) { + return; + } + const ctx = this.mouseTail.getContext('2d'); + if (!ctx || !this.tailPositions.length) { + return; + } + ctx.clearRect(0, 0, this.mouseTail.width, this.mouseTail.height); + ctx.beginPath(); + ctx.lineWidth = lineWidth; + ctx.lineCap = lineCap; + ctx.strokeStyle = strokeStyle; + ctx.moveTo(this.tailPositions[0].x, this.tailPositions[0].y); + this.tailPositions.forEach((p) => ctx.lineTo(p.x, p.y)); + ctx.stroke(); + }; + this.tailPositions.push(position); + draw(); + setTimeout(() => { + this.tailPositions = this.tailPositions.filter((p) => p !== position); + draw(); + }, duration / this.speedService.state.context.timer.speed); + } + hoverElements(el) { + var _a; + (_a = (this.lastHoveredRootNode || this.iframe.contentDocument)) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.\\:hover').forEach((hoveredEl) => { + hoveredEl.classList.remove(':hover'); + }); + this.lastHoveredRootNode = el.getRootNode(); + let currentEl = el; + while (currentEl) { + if (currentEl.classList) { + currentEl.classList.add(':hover'); + } + currentEl = currentEl.parentElement; + } + } + isUserInteraction(event) { + if (event.type !== EventType.IncrementalSnapshot) { + return false; + } + return (event.data.source > IncrementalSource.Mutation && + event.data.source <= IncrementalSource.Input); + } + backToNormal() { + this.nextUserInteractionEvent = null; + if (this.speedService.state.matches('normal')) { + return; + } + this.speedService.send({ type: 'BACK_TO_NORMAL' }); + this.emitter.emit(ReplayerEvents.SkipEnd, { + speed: this.speedService.state.context.normalSpeed, + }); + } + warnNodeNotFound(d, id) { + this.warn(`Node with id '${id}' not found. `, d); + } + warnCanvasMutationFailed(d, error) { + this.warn(`Has error on canvas update`, error, 'canvas mutation:', d); + } + debugNodeNotFound(d, id) { + this.debug(`Node with id '${id}' not found. `, d); + } + warn(...args) { + if (!this.config.showWarning) { + return; + } + this.config.logger.warn(REPLAY_CONSOLE_PREFIX, ...args); + } + debug(...args) { + if (!this.config.showDebug) { + return; + } + this.config.logger.log(REPLAY_CONSOLE_PREFIX, ...args); + } +} + +const { addCustomEvent } = record; +const { freezePage } = record; + +// DEFLATE is a complex format; to read this code, you should probably check the RFC first: + +// aliases for shorter compressed code (most minifers don't do this) +var u8 = Uint8Array, u16 = Uint16Array, u32 = Uint32Array; +// fixed length extra bits +var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]); +// fixed distance extra bits +// see fleb note +var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]); +// code length index map +var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); +// get base, reverse index map from extra bits +var freb = function (eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + // numbers here are at max 18 bits + var r = new u32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = ((j - b[i]) << 5) | i; + } + } + return [b, r]; +}; +var _a = freb(fleb, 2), fl = _a[0], revfl = _a[1]; +// we can ignore the fact that the other numbers are wrong; they never happen anyway +fl[28] = 258, revfl[258] = 28; +var _b = freb(fdeb, 0), fd = _b[0], revfd = _b[1]; +// map of value to reverse (assuming 16 bits) +var rev = new u16(32768); +for (var i = 0; i < 32768; ++i) { + // reverse table algorithm from SO + var x = ((i & 0xAAAA) >>> 1) | ((i & 0x5555) << 1); + x = ((x & 0xCCCC) >>> 2) | ((x & 0x3333) << 2); + x = ((x & 0xF0F0) >>> 4) | ((x & 0x0F0F) << 4); + rev[i] = (((x & 0xFF00) >>> 8) | ((x & 0x00FF) << 8)) >>> 1; +} +// create huffman tree from u8 "map": index -> code length for code index +// mb (max bits) must be at most 15 +// TODO: optimize/split up? +var hMap = (function (cd, mb, r) { + var s = cd.length; + // index + var i = 0; + // u16 "map": index -> # of codes with bit length = index + var l = new u16(mb); + // length of cd must be 288 (total # of codes) + for (; i < s; ++i) + ++l[cd[i] - 1]; + // u16 "map": index -> minimum code for bit length = index + var le = new u16(mb); + for (i = 0; i < mb; ++i) { + le[i] = (le[i - 1] + l[i - 1]) << 1; + } + var co; + if (r) { + // u16 "map": index -> number of actual bits, symbol for code + co = new u16(1 << mb); + // bits to remove for reverser + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + // ignore 0 lengths + if (cd[i]) { + // num encoding both symbol and bits read + var sv = (i << 4) | cd[i]; + // free bits + var r_1 = mb - cd[i]; + // start value + var v = le[cd[i] - 1]++ << r_1; + // m is end value + for (var m = v | ((1 << r_1) - 1); v <= m; ++v) { + // every 16 bit value starting with the code yields the same result + co[rev[v] >>> rvb] = sv; + } + } + } + } + else { + co = new u16(s); + for (i = 0; i < s; ++i) + co[i] = rev[le[cd[i] - 1]++] >>> (15 - cd[i]); + } + return co; +}); +// fixed length tree +var flt = new u8(288); +for (var i = 0; i < 144; ++i) + flt[i] = 8; +for (var i = 144; i < 256; ++i) + flt[i] = 9; +for (var i = 256; i < 280; ++i) + flt[i] = 7; +for (var i = 280; i < 288; ++i) + flt[i] = 8; +// fixed distance tree +var fdt = new u8(32); +for (var i = 0; i < 32; ++i) + fdt[i] = 5; +// fixed length map +var flm = /*#__PURE__*/ hMap(flt, 9, 0), flrm = /*#__PURE__*/ hMap(flt, 9, 1); +// fixed distance map +var fdm = /*#__PURE__*/ hMap(fdt, 5, 0), fdrm = /*#__PURE__*/ hMap(fdt, 5, 1); +// find max of array +var max = function (a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; +}; +// read d, starting at bit p and mask with m +var bits = function (d, p, m) { + var o = (p / 8) >> 0; + return ((d[o] | (d[o + 1] << 8)) >>> (p & 7)) & m; +}; +// read d, starting at bit p continuing for at least 16 bits +var bits16 = function (d, p) { + var o = (p / 8) >> 0; + return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >>> (p & 7)); +}; +// get end of byte +var shft = function (p) { return ((p / 8) >> 0) + (p & 7 && 1); }; +// typed array slice - allows garbage collector to free original reference, +// while being more compatible than .slice +var slc = function (v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + // can't use .constructor in case user-supplied + var n = new (v instanceof u16 ? u16 : v instanceof u32 ? u32 : u8)(e - s); + n.set(v.subarray(s, e)); + return n; +}; +// expands raw DEFLATE data +var inflt = function (dat, buf, st) { + // source length + var sl = dat.length; + // have to estimate size + var noBuf = !buf || st; + // no state + var noSt = !st || st.i; + if (!st) + st = {}; + // Assumes roughly 33% compression ratio average + if (!buf) + buf = new u8(sl * 3); + // ensure buffer can fit at least l elements + var cbuf = function (l) { + var bl = buf.length; + // need to increase size to fit + if (l > bl) { + // Double or set to necessary, whichever is greater + var nbuf = new u8(Math.max(bl * 2, l)); + nbuf.set(buf); + buf = nbuf; + } + }; + // last chunk bitpos bytes + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + // total bits + var tbts = sl * 8; + do { + if (!lm) { + // BFINAL - this is only 1 when last chunk is next + st.f = final = bits(dat, pos, 1); + // type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + // go to end of byte boundary + var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l; + if (t > sl) { + if (noSt) + throw 'unexpected EOF'; + break; + } + // ensure size + if (noBuf) + cbuf(bt + l); + // Copy over uncompressed data + buf.set(dat.subarray(s, t), bt); + // Get new bitpos, update byte count + st.b = bt += l, st.p = pos = t * 8; + continue; + } + else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + // literal lengths + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + // length+distance tree + var ldt = new u8(tl); + // code length tree + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + // use index map to get real code + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + // code lengths bits + var clb = max(clt), clbmsk = (1 << clb) - 1; + if (!noSt && pos + tl * (clb + 7) > tbts) + break; + // code lengths map + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl;) { + var r = clm[bits(dat, pos, clbmsk)]; + // bits read + pos += r & 15; + // symbol + var s = r >>> 4; + // code length to copy + if (s < 16) { + ldt[i++] = s; + } + else { + // copy count + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + // length tree distance tree + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + // max length bits + lbt = max(lt); + // max dist bits + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } + else + throw 'invalid block type'; + if (pos > tbts) + throw 'unexpected EOF'; + } + // Make sure the buffer can hold this + the largest possible addition + // Maximum chunk size (practically, theoretically infinite) is 2^17; + if (noBuf) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var mxa = lbt + dbt + 18; + while (noSt || pos + mxa < tbts) { + // bits read, code + var c = lm[bits16(dat, pos) & lms], sym = c >>> 4; + pos += c & 15; + if (pos > tbts) + throw 'unexpected EOF'; + if (!c) + throw 'invalid length/literal'; + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lm = null; + break; + } + else { + var add = sym - 254; + // no extra bits needed if less + if (sym > 264) { + // index + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + // dist + var d = dm[bits16(dat, pos) & dms], dsym = d >>> 4; + if (!d) + throw 'invalid distance'; + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & ((1 << b) - 1), pos += b; + } + if (pos > tbts) + throw 'unexpected EOF'; + if (noBuf) + cbuf(bt + 131072); + var end = bt + add; + for (; bt < end; bt += 4) { + buf[bt] = buf[bt - dt]; + buf[bt + 1] = buf[bt + 1 - dt]; + buf[bt + 2] = buf[bt + 2 - dt]; + buf[bt + 3] = buf[bt + 3 - dt]; + } + bt = end; + } + } + st.l = lm, st.p = pos, st.b = bt; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt == buf.length ? buf : slc(buf, 0, bt); +}; +// starting at p, write the minimum number of bits that can hold v to d +var wbits = function (d, p, v) { + v <<= p & 7; + var o = (p / 8) >> 0; + d[o] |= v; + d[o + 1] |= v >>> 8; +}; +// starting at p, write the minimum number of bits (>8) that can hold v to d +var wbits16 = function (d, p, v) { + v <<= p & 7; + var o = (p / 8) >> 0; + d[o] |= v; + d[o + 1] |= v >>> 8; + d[o + 2] |= v >>> 16; +}; +// creates code lengths from a frequency table +var hTree = function (d, mb) { + // Need extra info to make a tree + var t = []; + for (var i = 0; i < d.length; ++i) { + if (d[i]) + t.push({ s: i, f: d[i] }); + } + var s = t.length; + var t2 = t.slice(); + if (!s) + return [new u8(0), 0]; + if (s == 1) { + var v = new u8(t[0].s + 1); + v[t[0].s] = 1; + return [v, 1]; + } + t.sort(function (a, b) { return a.f - b.f; }); + // after i2 reaches last ind, will be stopped + // freq must be greater than largest possible number of symbols + t.push({ s: -1, f: 25001 }); + var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2; + t[0] = { s: -1, f: l.f + r.f, l: l, r: r }; + // efficient algorithm from UZIP.js + // i0 is lookbehind, i2 is lookahead - after processing two low-freq + // symbols that combined have high freq, will start processing i2 (high-freq, + // non-composite) symbols instead + // see https://reddit.com/r/photopea/comments/ikekht/uzipjs_questions/ + while (i1 != s - 1) { + l = t[t[i0].f < t[i2].f ? i0++ : i2++]; + r = t[i0 != i1 && t[i0].f < t[i2].f ? i0++ : i2++]; + t[i1++] = { s: -1, f: l.f + r.f, l: l, r: r }; + } + var maxSym = t2[0].s; + for (var i = 1; i < s; ++i) { + if (t2[i].s > maxSym) + maxSym = t2[i].s; + } + // code lengths + var tr = new u16(maxSym + 1); + // max bits in tree + var mbt = ln(t[i1 - 1], tr, 0); + if (mbt > mb) { + // more algorithms from UZIP.js + // TODO: find out how this code works (debt) + // ind debt + var i = 0, dt = 0; + // left cost + var lft = mbt - mb, cst = 1 << lft; + t2.sort(function (a, b) { return tr[b.s] - tr[a.s] || a.f - b.f; }); + for (; i < s; ++i) { + var i2_1 = t2[i].s; + if (tr[i2_1] > mb) { + dt += cst - (1 << (mbt - tr[i2_1])); + tr[i2_1] = mb; + } + else + break; + } + dt >>>= lft; + while (dt > 0) { + var i2_2 = t2[i].s; + if (tr[i2_2] < mb) + dt -= 1 << (mb - tr[i2_2]++ - 1); + else + ++i; + } + for (; i >= 0 && dt; --i) { + var i2_3 = t2[i].s; + if (tr[i2_3] == mb) { + --tr[i2_3]; + ++dt; + } + } + mbt = mb; + } + return [new u8(tr), mbt]; +}; +// get the max length and assign length codes +var ln = function (n, l, d) { + return n.s == -1 + ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) + : (l[n.s] = d); +}; +// length codes generation +var lc = function (c) { + var s = c.length; + // Note that the semicolon was intentional + while (s && !c[--s]) + ; + var cl = new u16(++s); + // ind num streak + var cli = 0, cln = c[0], cls = 1; + var w = function (v) { cl[cli++] = v; }; + for (var i = 1; i <= s; ++i) { + if (c[i] == cln && i != s) + ++cls; + else { + if (!cln && cls > 2) { + for (; cls > 138; cls -= 138) + w(32754); + if (cls > 2) { + w(cls > 10 ? ((cls - 11) << 5) | 28690 : ((cls - 3) << 5) | 12305); + cls = 0; + } + } + else if (cls > 3) { + w(cln), --cls; + for (; cls > 6; cls -= 6) + w(8304); + if (cls > 2) + w(((cls - 3) << 5) | 8208), cls = 0; + } + while (cls--) + w(cln); + cls = 1; + cln = c[i]; + } + } + return [cl.subarray(0, cli), s]; +}; +// calculate the length of output from tree, code lengths +var clen = function (cf, cl) { + var l = 0; + for (var i = 0; i < cl.length; ++i) + l += cf[i] * cl[i]; + return l; +}; +// writes a fixed block +// returns the new bit pos +var wfblk = function (out, pos, dat) { + // no need to write 00 as type: TypedArray defaults to 0 + var s = dat.length; + var o = shft(pos + 2); + out[o] = s & 255; + out[o + 1] = s >>> 8; + out[o + 2] = out[o] ^ 255; + out[o + 3] = out[o + 1] ^ 255; + for (var i = 0; i < s; ++i) + out[o + i + 4] = dat[i]; + return (o + 4 + s) * 8; +}; +// writes a block +var wblk = function (dat, out, final, syms, lf, df, eb, li, bs, bl, p) { + wbits(out, p++, final); + ++lf[256]; + var _a = hTree(lf, 15), dlt = _a[0], mlb = _a[1]; + var _b = hTree(df, 15), ddt = _b[0], mdb = _b[1]; + var _c = lc(dlt), lclt = _c[0], nlc = _c[1]; + var _d = lc(ddt), lcdt = _d[0], ndc = _d[1]; + var lcfreq = new u16(19); + for (var i = 0; i < lclt.length; ++i) + lcfreq[lclt[i] & 31]++; + for (var i = 0; i < lcdt.length; ++i) + lcfreq[lcdt[i] & 31]++; + var _e = hTree(lcfreq, 7), lct = _e[0], mlcb = _e[1]; + var nlcc = 19; + for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) + ; + var flen = (bl + 5) << 3; + var ftlen = clen(lf, flt) + clen(df, fdt) + eb; + var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + (2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]); + if (flen <= ftlen && flen <= dtlen) + return wfblk(out, p, dat.subarray(bs, bs + bl)); + var lm, ll, dm, dl; + wbits(out, p, 1 + (dtlen < ftlen)), p += 2; + if (dtlen < ftlen) { + lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; + var llm = hMap(lct, mlcb, 0); + wbits(out, p, nlc - 257); + wbits(out, p + 5, ndc - 1); + wbits(out, p + 10, nlcc - 4); + p += 14; + for (var i = 0; i < nlcc; ++i) + wbits(out, p + 3 * i, lct[clim[i]]); + p += 3 * nlcc; + var lcts = [lclt, lcdt]; + for (var it = 0; it < 2; ++it) { + var clct = lcts[it]; + for (var i = 0; i < clct.length; ++i) { + var len = clct[i] & 31; + wbits(out, p, llm[len]), p += lct[len]; + if (len > 15) + wbits(out, p, (clct[i] >>> 5) & 127), p += clct[i] >>> 12; + } + } + } + else { + lm = flm, ll = flt, dm = fdm, dl = fdt; + } + for (var i = 0; i < li; ++i) { + if (syms[i] > 255) { + var len = (syms[i] >>> 18) & 31; + wbits16(out, p, lm[len + 257]), p += ll[len + 257]; + if (len > 7) + wbits(out, p, (syms[i] >>> 23) & 31), p += fleb[len]; + var dst = syms[i] & 31; + wbits16(out, p, dm[dst]), p += dl[dst]; + if (dst > 3) + wbits16(out, p, (syms[i] >>> 5) & 8191), p += fdeb[dst]; + } + else { + wbits16(out, p, lm[syms[i]]), p += ll[syms[i]]; + } + } + wbits16(out, p, lm[256]); + return p + ll[256]; +}; +// deflate options (nice << 13) | chain +var deo = /*#__PURE__*/ new u32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]); +// empty +var et = /*#__PURE__*/ new u8(0); +// compresses data into a raw DEFLATE buffer +var dflt = function (dat, lvl, plvl, pre, post, lst) { + var s = dat.length; + var o = new u8(pre + s + 5 * (1 + Math.floor(s / 7000)) + post); + // writing to this writes to the output buffer + var w = o.subarray(pre, o.length - post); + var pos = 0; + if (!lvl || s < 8) { + for (var i = 0; i <= s; i += 65535) { + // end + var e = i + 65535; + if (e < s) { + // write full block + pos = wfblk(w, pos, dat.subarray(i, e)); + } + else { + // write final block + w[i] = lst; + pos = wfblk(w, pos, dat.subarray(i, s)); + } + } + } + else { + var opt = deo[lvl - 1]; + var n = opt >>> 13, c = opt & 8191; + var msk_1 = (1 << plvl) - 1; + // prev 2-byte val map curr 2-byte val map + var prev = new u16(32768), head = new u16(msk_1 + 1); + var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; + var hsh = function (i) { return (dat[i] ^ (dat[i + 1] << bs1_1) ^ (dat[i + 2] << bs2_1)) & msk_1; }; + // 24576 is an arbitrary number of maximum symbols per block + // 424 buffer for last block + var syms = new u32(25000); + // length/literal freq distance freq + var lf = new u16(288), df = new u16(32); + // l/lcnt exbits index l/lind waitdx bitpos + var lc_1 = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0; + for (; i < s; ++i) { + // hash value + var hv = hsh(i); + // index mod 32768 + var imod = i & 32767; + // previous index with this value + var pimod = head[hv]; + prev[imod] = pimod; + head[hv] = imod; + // We always should modify head and prev, but only add symbols if + // this data is not yet processed ("wait" for wait index) + if (wi <= i) { + // bytes remaining + var rem = s - i; + if ((lc_1 > 7000 || li > 24576) && rem > 423) { + pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos); + li = lc_1 = eb = 0, bs = i; + for (var j = 0; j < 286; ++j) + lf[j] = 0; + for (var j = 0; j < 30; ++j) + df[j] = 0; + } + // len dist chain + var l = 2, d = 0, ch_1 = c, dif = (imod - pimod) & 32767; + if (rem > 2 && hv == hsh(i - dif)) { + var maxn = Math.min(n, rem) - 1; + var maxd = Math.min(32767, i); + // max possible length + // not capped at dif because decompressors implement "rolling" index population + var ml = Math.min(258, rem); + while (dif <= maxd && --ch_1 && imod != pimod) { + if (dat[i + l] == dat[i + l - dif]) { + var nl = 0; + for (; nl < ml && dat[i + nl] == dat[i + nl - dif]; ++nl) + ; + if (nl > l) { + l = nl, d = dif; + // break out early when we reach "nice" (we are satisfied enough) + if (nl > maxn) + break; + // now, find the rarest 2-byte sequence within this + // length of literals and search for that instead. + // Much faster than just using the start + var mmd = Math.min(dif, nl - 2); + var md = 0; + for (var j = 0; j < mmd; ++j) { + var ti = (i - dif + j + 32768) & 32767; + var pti = prev[ti]; + var cd = (ti - pti + 32768) & 32767; + if (cd > md) + md = cd, pimod = ti; + } + } + } + // check the previous match + imod = pimod, pimod = prev[imod]; + dif += (imod - pimod + 32768) & 32767; + } + } + // d will be nonzero only when a match was found + if (d) { + // store both dist and len data in one Uint32 + // Make sure this is recognized as a len/dist with 28th bit (2^28) + syms[li++] = 268435456 | (revfl[l] << 18) | revfd[d]; + var lin = revfl[l] & 31, din = revfd[d] & 31; + eb += fleb[lin] + fdeb[din]; + ++lf[257 + lin]; + ++df[din]; + wi = i + l; + ++lc_1; + } + else { + syms[li++] = dat[i]; + ++lf[dat[i]]; + } + } + } + pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos); + // this is the easiest way to avoid needing to maintain state + if (!lst) + pos = wfblk(w, pos, et); + } + return slc(o, 0, pre + shft(pos) + post); +}; +// Alder32 +var adler = function () { + var a = 1, b = 0; + return { + p: function (d) { + // closures have awful performance + var n = a, m = b; + var l = d.length; + for (var i = 0; i != l;) { + var e = Math.min(i + 5552, l); + for (; i < e; ++i) + n += d[i], m += n; + n %= 65521, m %= 65521; + } + a = n, b = m; + }, + d: function () { return ((a >>> 8) << 16 | (b & 255) << 8 | (b >>> 8)) + ((a & 255) << 23) * 2; } + }; +}; +// deflate with opts +var dopt = function (dat, opt, pre, post, st) { + return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : (12 + opt.mem), pre, post, !st); +}; +// write bytes +var wbytes = function (d, b, v) { + for (; v; ++b) + d[b] = v, v >>>= 8; +}; +// zlib header +var zlh = function (c, o) { + var lv = o.level, fl = lv == 0 ? 0 : lv < 6 ? 1 : lv == 9 ? 3 : 2; + c[0] = 120, c[1] = (fl << 6) | (fl ? (32 - 2 * fl) : 1); +}; +// zlib valid +var zlv = function (d) { + if ((d[0] & 15) != 8 || (d[0] >>> 4) > 7 || ((d[0] << 8 | d[1]) % 31)) + throw 'invalid zlib data'; + if (d[1] & 32) + throw 'invalid zlib data: preset dictionaries not supported'; +}; +/** + * Compress data with Zlib + * @param data The data to compress + * @param opts The compression options + * @returns The zlib-compressed version of the data + */ +function zlibSync(data, opts) { + if (opts === void 0) { opts = {}; } + var a = adler(); + a.p(data); + var d = dopt(data, opts, 2, 4); + return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d; +} +/** + * Expands Zlib data + * @param data The data to decompress + * @param out Where to write the data. Saves memory if you know the decompressed size and provide an output buffer of that length. + * @returns The decompressed version of the data + */ +function unzlibSync(data, out) { + return inflt((zlv(data), data.subarray(2, -4)), out); +} +/** + * Converts a string into a Uint8Array for use with compression/decompression methods + * @param str The string to encode + * @param latin1 Whether or not to interpret the data as Latin-1. This should + * not need to be true unless decoding a binary string. + * @returns The string encoded in UTF-8/Latin-1 binary + */ +function strToU8(str, latin1) { + var l = str.length; + if (!latin1 && typeof TextEncoder != 'undefined') + return new TextEncoder().encode(str); + var ar = new u8(str.length + (str.length >>> 1)); + var ai = 0; + var w = function (v) { ar[ai++] = v; }; + for (var i = 0; i < l; ++i) { + if (ai + 5 > ar.length) { + var n = new u8(ai + 8 + ((l - i) << 1)); + n.set(ar); + ar = n; + } + var c = str.charCodeAt(i); + if (c < 128 || latin1) + w(c); + else if (c < 2048) + w(192 | (c >>> 6)), w(128 | (c & 63)); + else if (c > 55295 && c < 57344) + c = 65536 + (c & 1023 << 10) | (str.charCodeAt(++i) & 1023), + w(240 | (c >>> 18)), w(128 | ((c >>> 12) & 63)), w(128 | ((c >>> 6) & 63)), w(128 | (c & 63)); + else + w(224 | (c >>> 12)), w(128 | ((c >>> 6) & 63)), w(128 | (c & 63)); + } + return slc(ar, 0, ai); +} +/** + * Converts a Uint8Array to a string + * @param dat The data to decode to string + * @param latin1 Whether or not to interpret the data as Latin-1. This should + * not need to be true unless encoding to binary string. + * @returns The original UTF-8/Latin-1 string + */ +function strFromU8(dat, latin1) { + var r = ''; + if (!latin1 && typeof TextDecoder != 'undefined') + return new TextDecoder().decode(dat); + for (var i = 0; i < dat.length;) { + var c = dat[i++]; + if (c < 128 || latin1) + r += String.fromCharCode(c); + else if (c < 224) + r += String.fromCharCode((c & 31) << 6 | (dat[i++] & 63)); + else if (c < 240) + r += String.fromCharCode((c & 15) << 12 | (dat[i++] & 63) << 6 | (dat[i++] & 63)); + else + c = ((c & 15) << 18 | (dat[i++] & 63) << 12 | (dat[i++] & 63) << 6 | (dat[i++] & 63)) - 65536, + r += String.fromCharCode(55296 | (c >> 10), 56320 | (c & 1023)); + } + return r; +} + +const MARK = 'v1'; + +const pack = (event) => { + const _e = Object.assign(Object.assign({}, event), { v: MARK }); + return strFromU8(zlibSync(strToU8(JSON.stringify(_e))), true); +}; + +const unpack = (raw) => { + if (typeof raw !== 'string') { + return raw; + } + try { + const e = JSON.parse(raw); + if (e.timestamp) { + return e; + } + } + catch (error) { + } + try { + const e = JSON.parse(strFromU8(unzlibSync(strToU8(raw, true)))); + if (e.v === MARK) { + return e; + } + throw new Error(`These events were packed with packer ${e.v} which is incompatible with current packer ${MARK}.`); + } + catch (error) { + console.error(error); + throw new Error('Unknown data format.'); + } +}; + +class StackFrame { + constructor(obj) { + this.fileName = obj.fileName || ''; + this.functionName = obj.functionName || ''; + this.lineNumber = obj.lineNumber; + this.columnNumber = obj.columnNumber; + } + toString() { + const lineNumber = this.lineNumber || ''; + const columnNumber = this.columnNumber || ''; + if (this.functionName) + return `${this.functionName} (${this.fileName}:${lineNumber}:${columnNumber})`; + return `${this.fileName}:${lineNumber}:${columnNumber}`; + } +} +const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; +const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; +const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; +const ErrorStackParser = { + parse: function (error) { + if (!error) { + return []; + } + if (typeof error.stacktrace !== 'undefined' || + typeof error['opera#sourceloc'] !== 'undefined') { + return this.parseOpera(error); + } + else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { + return this.parseV8OrIE(error); + } + else if (error.stack) { + return this.parseFFOrSafari(error); + } + else { + console.warn('[console-record-plugin]: Failed to parse error object:', error); + return []; + } + }, + extractLocation: function (urlLike) { + if (urlLike.indexOf(':') === -1) { + return [urlLike]; + } + const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; + const parts = regExp.exec(urlLike.replace(/[()]/g, '')); + if (!parts) + throw new Error(`Cannot parse given url: ${urlLike}`); + return [parts[1], parts[2] || undefined, parts[3] || undefined]; + }, + parseV8OrIE: function (error) { + const filtered = error.stack.split('\n').filter(function (line) { + return !!line.match(CHROME_IE_STACK_REGEXP); + }, this); + return filtered.map(function (line) { + if (line.indexOf('(eval ') > -1) { + line = line + .replace(/eval code/g, 'eval') + .replace(/(\(eval at [^()]*)|(\),.*$)/g, ''); + } + let sanitizedLine = line.replace(/^\s+/, '').replace(/\(eval code/g, '('); + const location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/); + sanitizedLine = location + ? sanitizedLine.replace(location[0], '') + : sanitizedLine; + const tokens = sanitizedLine.split(/\s+/).slice(1); + const locationParts = this.extractLocation(location ? location[1] : tokens.pop()); + const functionName = tokens.join(' ') || undefined; + const fileName = ['eval', ''].indexOf(locationParts[0]) > -1 + ? undefined + : locationParts[0]; + return new StackFrame({ + functionName, + fileName, + lineNumber: locationParts[1], + columnNumber: locationParts[2], + }); + }, this); + }, + parseFFOrSafari: function (error) { + const filtered = error.stack.split('\n').filter(function (line) { + return !line.match(SAFARI_NATIVE_CODE_REGEXP); + }, this); + return filtered.map(function (line) { + if (line.indexOf(' > eval') > -1) { + line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1'); + } + if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { + return new StackFrame({ + functionName: line, + }); + } + else { + const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; + const matches = line.match(functionNameRegex); + const functionName = matches && matches[1] ? matches[1] : undefined; + const locationParts = this.extractLocation(line.replace(functionNameRegex, '')); + return new StackFrame({ + functionName, + fileName: locationParts[0], + lineNumber: locationParts[1], + columnNumber: locationParts[2], + }); + } + }, this); + }, + parseOpera: function (e) { + if (!e.stacktrace || + (e.message.indexOf('\n') > -1 && + e.message.split('\n').length > e.stacktrace.split('\n').length)) { + return this.parseOpera9(e); + } + else if (!e.stack) { + return this.parseOpera10(e); + } + else { + return this.parseOpera11(e); + } + }, + parseOpera9: function (e) { + const lineRE = /Line (\d+).*script (?:in )?(\S+)/i; + const lines = e.message.split('\n'); + const result = []; + for (let i = 2, len = lines.length; i < len; i += 2) { + const match = lineRE.exec(lines[i]); + if (match) { + result.push(new StackFrame({ + fileName: match[2], + lineNumber: parseFloat(match[1]), + })); + } + } + return result; + }, + parseOpera10: function (e) { + const lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; + const lines = e.stacktrace.split('\n'); + const result = []; + for (let i = 0, len = lines.length; i < len; i += 2) { + const match = lineRE.exec(lines[i]); + if (match) { + result.push(new StackFrame({ + functionName: match[3] || undefined, + fileName: match[2], + lineNumber: parseFloat(match[1]), + })); + } + } + return result; + }, + parseOpera11: function (error) { + const filtered = error.stack.split('\n').filter(function (line) { + return (!!line.match(FIREFOX_SAFARI_STACK_REGEXP) && + !line.match(/^Error created at/)); + }, this); + return filtered.map(function (line) { + const tokens = line.split('@'); + const locationParts = this.extractLocation(tokens.pop()); + const functionCall = tokens.shift() || ''; + const functionName = functionCall + .replace(//, '$2') + .replace(/\([^)]*\)/g, '') || undefined; + return new StackFrame({ + functionName, + fileName: locationParts[0], + lineNumber: locationParts[1], + columnNumber: locationParts[2], + }); + }, this); + }, +}; + +function pathToSelector(node) { + if (!node || !node.outerHTML) { + return ''; + } + let path = ''; + while (node.parentElement) { + let name = node.localName; + if (!name) { + break; + } + name = name.toLowerCase(); + const parent = node.parentElement; + const domSiblings = []; + if (parent.children && parent.children.length > 0) { + for (let i = 0; i < parent.children.length; i++) { + const sibling = parent.children[i]; + if (sibling.localName && sibling.localName.toLowerCase) { + if (sibling.localName.toLowerCase() === name) { + domSiblings.push(sibling); + } + } + } + } + if (domSiblings.length > 1) { + name += `:eq(${domSiblings.indexOf(node)})`; + } + path = name + (path ? '>' + path : ''); + node = parent; + } + return path; +} +function isObject(obj) { + return Object.prototype.toString.call(obj) === '[object Object]'; +} +function isObjTooDeep(obj, limit) { + if (limit === 0) { + return true; + } + const keys = Object.keys(obj); + for (const key of keys) { + if (isObject(obj[key]) && + isObjTooDeep(obj[key], limit - 1)) { + return true; + } + } + return false; +} +function stringify(obj, stringifyOptions) { + const options = { + numOfKeysLimit: 50, + depthOfLimit: 4, + }; + Object.assign(options, stringifyOptions); + const stack = []; + const keys = []; + return JSON.stringify(obj, function (key, value) { + if (stack.length > 0) { + const thisPos = stack.indexOf(this); + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); + if (~stack.indexOf(value)) { + if (stack[0] === value) { + value = '[Circular ~]'; + } + else { + value = + '[Circular ~.' + + keys.slice(0, stack.indexOf(value)).join('.') + + ']'; + } + } + } + else { + stack.push(value); + } + if (value === null) + return value; + if (value === undefined) + return 'undefined'; + if (shouldIgnore(value)) { + return toString(value); + } + if (typeof value === 'bigint') { + return value.toString() + 'n'; + } + if (value instanceof Event) { + const eventResult = {}; + for (const eventKey in value) { + const eventValue = value[eventKey]; + if (Array.isArray(eventValue)) { + eventResult[eventKey] = pathToSelector((eventValue.length ? eventValue[0] : null)); + } + else { + eventResult[eventKey] = eventValue; + } + } + return eventResult; + } + else if (value instanceof Node) { + if (value instanceof HTMLElement) { + return value ? value.outerHTML : ''; + } + return value.nodeName; + } + else if (value instanceof Error) { + return value.stack + ? value.stack + '\nEnd of stack for Error object' + : value.name + ': ' + value.message; + } + return value; + }); + function shouldIgnore(_obj) { + if (isObject(_obj) && Object.keys(_obj).length > options.numOfKeysLimit) { + return true; + } + if (typeof _obj === 'function') { + return true; + } + if (isObject(_obj) && + isObjTooDeep(_obj, options.depthOfLimit)) { + return true; + } + return false; + } + function toString(_obj) { + let str = _obj.toString(); + if (options.stringLengthLimit && str.length > options.stringLengthLimit) { + str = `${str.slice(0, options.stringLengthLimit)}...`; + } + return str; + } +} + +const defaultLogOptions = { + level: [ + 'assert', + 'clear', + 'count', + 'countReset', + 'debug', + 'dir', + 'dirxml', + 'error', + 'group', + 'groupCollapsed', + 'groupEnd', + 'info', + 'log', + 'table', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'warn', + ], + lengthThreshold: 1000, + logger: 'console', +}; +function initLogObserver(cb, win, options) { + const logOptions = (options ? Object.assign({}, defaultLogOptions, options) : defaultLogOptions); + const loggerType = logOptions.logger; + if (!loggerType) { + return () => { + }; + } + let logger; + if (typeof loggerType === 'string') { + logger = win[loggerType]; + } + else { + logger = loggerType; + } + let logCount = 0; + let inStack = false; + const cancelHandlers = []; + if (logOptions.level.includes('error')) { + const errorHandler = (event) => { + const message = event.message, error = event.error; + const trace = ErrorStackParser.parse(error).map((stackFrame) => stackFrame.toString()); + const payload = [stringify(message, logOptions.stringifyOptions)]; + cb({ + level: 'error', + trace, + payload, + }); + }; + win.addEventListener('error', errorHandler); + cancelHandlers.push(() => { + win.removeEventListener('error', errorHandler); + }); + const unhandledrejectionHandler = (event) => { + let error; + let payload; + if (event.reason instanceof Error) { + error = event.reason; + payload = [ + stringify(`Uncaught (in promise) ${error.name}: ${error.message}`, logOptions.stringifyOptions), + ]; + } + else { + error = new Error(); + payload = [ + stringify('Uncaught (in promise)', logOptions.stringifyOptions), + stringify(event.reason, logOptions.stringifyOptions), + ]; + } + const trace = ErrorStackParser.parse(error).map((stackFrame) => stackFrame.toString()); + cb({ + level: 'error', + trace, + payload, + }); + }; + win.addEventListener('unhandledrejection', unhandledrejectionHandler); + cancelHandlers.push(() => { + win.removeEventListener('unhandledrejection', unhandledrejectionHandler); + }); + } + for (const levelType of logOptions.level) { + cancelHandlers.push(replace(logger, levelType)); + } + return () => { + cancelHandlers.forEach((h) => h()); + }; + function replace(_logger, level) { + if (!_logger[level]) { + return () => { + }; + } + return patch(_logger, level, (original) => { + return (...args) => { + original.apply(this, args); + if (inStack) { + return; + } + inStack = true; + try { + const trace = ErrorStackParser.parse(new Error()) + .map((stackFrame) => stackFrame.toString()) + .splice(1); + const payload = args.map((s) => stringify(s, logOptions.stringifyOptions)); + logCount++; + if (logCount < logOptions.lengthThreshold) { + cb({ + level, + trace, + payload, + }); + } + else if (logCount === logOptions.lengthThreshold) { + cb({ + level: 'warn', + trace: [], + payload: [ + stringify('The number of log records reached the threshold.'), + ], + }); + } + } + catch (error) { + original('rrweb logger error:', error, ...args); + } + finally { + inStack = false; + } + }; + }); + } +} +const PLUGIN_NAME = 'rrweb/console@1'; +const getRecordConsolePlugin = (options) => ({ + name: PLUGIN_NAME, + observer: initLogObserver, + options: options, +}); + +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +const defaultLogConfig = { + level: [ + 'assert', + 'clear', + 'count', + 'countReset', + 'debug', + 'dir', + 'dirxml', + 'error', + 'group', + 'groupCollapsed', + 'groupEnd', + 'info', + 'log', + 'table', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'warn', + ], + replayLogger: undefined, +}; +class LogReplayPlugin { + constructor(config) { + this.config = Object.assign(defaultLogConfig, config); + } + getConsoleLogger() { + const replayLogger = {}; + for (const level of this.config.level) { + if (level === 'trace') { + replayLogger[level] = (data) => { + const logger = console.log[ORIGINAL_ATTRIBUTE_NAME] + ? console.log[ORIGINAL_ATTRIBUTE_NAME] + : console.log; + logger(...data.payload.map((s) => JSON.parse(s)), this.formatMessage(data)); + }; + } + else { + replayLogger[level] = (data) => { + const logger = console[level][ORIGINAL_ATTRIBUTE_NAME] + ? console[level][ORIGINAL_ATTRIBUTE_NAME] + : console[level]; + logger(...data.payload.map((s) => JSON.parse(s)), this.formatMessage(data)); + }; + } + } + return replayLogger; + } + formatMessage(data) { + if (data.trace.length === 0) { + return ''; + } + const stackPrefix = '\n\tat '; + let result = stackPrefix; + result += data.trace.join(stackPrefix); + return result; + } +} +const getReplayConsolePlugin = (options) => { + const replayLogger = (options === null || options === void 0 ? void 0 : options.replayLogger) || new LogReplayPlugin(options).getConsoleLogger(); + return { + handler(event, _isSync, context) { + let logData = null; + if (event.type === EventType.IncrementalSnapshot && + event.data.source === IncrementalSource.Log) { + logData = event.data; + } + else if (event.type === EventType.Plugin && + event.data.plugin === PLUGIN_NAME) { + logData = event.data.payload; + } + if (logData) { + try { + if (typeof replayLogger[logData.level] === 'function') { + replayLogger[logData.level](logData); + } + } + catch (error) { + if (context.replayer.config.showWarning) { + console.warn(error); + } + } + } + }, + }; +}; + +exports.EventType = EventType; +exports.IncrementalSource = IncrementalSource; +exports.MouseInteractions = MouseInteractions; +exports.PLUGIN_NAME = PLUGIN_NAME; +exports.Replayer = Replayer; +exports.ReplayerEvents = ReplayerEvents; +exports.addCustomEvent = addCustomEvent; +exports.canvasMutation = canvasMutation; +exports.freezePage = freezePage; +exports.getRecordConsolePlugin = getRecordConsolePlugin; +exports.getReplayConsolePlugin = getReplayConsolePlugin; +exports.pack = pack; +exports.record = record; +exports.unpack = unpack; +exports.utils = utils; + +},{}],4:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -155,7 +10974,7 @@ exports.REMOVE_ACTION = REMOVE_ACTION; exports.DELETE_ACTION = DELETE_ACTION; exports.apiActions = apiActions; -},{"./utils":14}],3:[function(require,module,exports){ +},{"./utils":18}],5:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -163,13 +10982,13 @@ Object.defineProperty(exports, '__esModule', { }); var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; exports['default'] = Config; module.exports = exports['default']; -},{}],4:[function(require,module,exports){ +},{}],6:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -332,7 +11151,7 @@ FormTracker.prototype.after_track_handler = function (props, options) { exports.FormTracker = FormTracker; exports.LinkTracker = LinkTracker; -},{"./utils":14}],5:[function(require,module,exports){ +},{"./utils":18}],7:[function(require,module,exports){ /** * GDPR utils * @@ -645,7 +11464,42 @@ function _addOptOutCheck(method, getConfigValue) { }; } -},{"./utils":14}],6:[function(require,module,exports){ +},{"./utils":18}],8:[function(require,module,exports){ +// For loading separate bundles asynchronously via script tag +// so that we don't load them until they are needed at runtime. +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports.loadAsync = loadAsync; +exports.loadNoop = loadNoop; +exports.loadThrowError = loadThrowError; + +function loadAsync(src, onload) { + var scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + scriptEl.async = true; + scriptEl.onload = onload; + scriptEl.src = src; + document.head.appendChild(scriptEl); +} + +// For builds that have everything in one bundle, no extra work. + +function loadNoop(_src, onload) { + onload(); +} + +// For builds that do NOT want any extra bundles (e.g. session recorder) +// and just the main SDK, throw an error when trying to load a separate bundle. +// eslint-disable-next-line no-unused-vars + +function loadThrowError(src, _onload) { + throw new Error('This build of Mixpanel only includes core SDK functionality, could not load ' + src); +} + +},{}],9:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -653,14 +11507,18 @@ Object.defineProperty(exports, '__esModule', { value: true }); +require('../recorder'); + var _mixpanelCore = require('../mixpanel-core'); -var mixpanel = (0, _mixpanelCore.init_as_module)(); +var _bundleLoaders = require('./bundle-loaders'); + +var mixpanel = (0, _mixpanelCore.init_as_module)(_bundleLoaders.loadNoop); exports['default'] = mixpanel; module.exports = exports['default']; -},{"../mixpanel-core":7}],7:[function(require,module,exports){ +},{"../mixpanel-core":10,"../recorder":14,"./bundle-loaders":8}],10:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -718,6 +11576,12 @@ Globals should be all caps */ var init_type; // MODULE or SNIPPET loader +// allow bundlers to specify how extra code (recorder bundle) should be loaded +// eslint-disable-next-line no-unused-vars +var load_extra_bundle = function load_extra_bundle(src, _onload) { + throw new Error(src + ' not available in this build.'); +}; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -812,7 +11676,9 @@ var DEFAULT_CONFIG = { 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': _utils.MAX_RECORDING_MS, @@ -1047,12 +11913,7 @@ MixpanelLib.prototype.start_session_recording = (0, _gdprUtils.addOptOutCheckMix }, this); if (_utils._.isUndefined(_utils.window['__mp_recorder'])) { - var scriptEl = _utils.document.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - _utils.document.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -1356,7 +12217,8 @@ MixpanelLib.prototype._send_request = function (url, data, options, callback) { lib.report_error(error); if (callback) { if (verbose_mode) { - callback({ status: 0, error: error, xhr_req: req }); + var response_headers = req['responseHeaders'] || {}; + callback({ status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After'] }); } else { callback(0); } @@ -1457,14 +12319,15 @@ MixpanelLib.prototype.init_batchers = function () { var batcher_for = _utils._.bind(function (attrs) { return new _requestBatcher.RequestBatcher(attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _utils._.bind(function (data, options, cb) { this._send_request(this.get_config('api_host') + attrs.endpoint, this._encode_data_for_request(data), options, this._prepare_callback(cb, data)); }, this), beforeSendHook: _utils._.bind(function (item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _utils._.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _utils._.bind(this.stop_batch_senders, this), + usePersistence: true }); }, this); var batcher_configs = this.get_batcher_configs(); @@ -2886,7 +13749,8 @@ var add_dom_loaded_handler = function add_dom_loaded_handler() { _utils._.register_event(_utils.window, 'load', dom_loaded_handler, true); }; -function init_from_snippet() { +function init_from_snippet(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_SNIPPET; mixpanel_master = _utils.window[PRIMARY_INSTANCE_NAME]; @@ -2926,7 +13790,8 @@ function init_from_snippet() { add_dom_loaded_handler(); } -function init_as_module() { +function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); @@ -2937,7 +13802,7 @@ function init_as_module() { return mixpanel_master; } -},{"./config":3,"./dom-trackers":4,"./gdpr-utils":5,"./mixpanel-group":8,"./mixpanel-people":9,"./mixpanel-persistence":10,"./request-batcher":11,"./utils":14}],8:[function(require,module,exports){ +},{"./config":5,"./dom-trackers":6,"./gdpr-utils":7,"./mixpanel-group":11,"./mixpanel-people":12,"./mixpanel-persistence":13,"./request-batcher":15,"./utils":18}],11:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -3121,7 +13986,7 @@ MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; exports.MixpanelGroup = MixpanelGroup; -},{"./api-actions":2,"./gdpr-utils":5,"./utils":14}],9:[function(require,module,exports){ +},{"./api-actions":4,"./gdpr-utils":7,"./utils":18}],12:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -3601,7 +14466,7 @@ MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; exports.MixpanelPeople = MixpanelPeople; -},{"./api-actions":2,"./gdpr-utils":5,"./utils":14}],10:[function(require,module,exports){ +},{"./api-actions":4,"./gdpr-utils":7,"./utils":18}],13:[function(require,module,exports){ /* eslint camelcase: "off" */ 'use strict'; @@ -4031,7 +14896,248 @@ exports.PEOPLE_DISTINCT_ID_KEY = PEOPLE_DISTINCT_ID_KEY; exports.ALIAS_ID_KEY = ALIAS_ID_KEY; exports.EVENT_TIMERS_KEY = EVENT_TIMERS_KEY; -},{"./api-actions":2,"./utils":14}],11:[function(require,module,exports){ +},{"./api-actions":4,"./utils":18}],14:[function(require,module,exports){ +'use strict'; + +var _rrweb = require('rrweb'); + +var _rrwebTypes = require('@rrweb/types'); + +var _utils = require('../utils'); + +// eslint-disable-line camelcase + +var _gdprUtils = require('../gdpr-utils'); + +var _requestBatcher = require('../request-batcher'); + +var logger = (0, _utils.console_with_prefix)('recorder'); +var CompressionStream = _utils.window['CompressionStream']; + +var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true +}; + +var ACTIVE_SOURCES = new Set([_rrwebTypes.IncrementalSource.MouseMove, _rrwebTypes.IncrementalSource.MouseInteraction, _rrwebTypes.IncrementalSource.Scroll, _rrwebTypes.IncrementalSource.ViewportResize, _rrwebTypes.IncrementalSource.Input, _rrwebTypes.IncrementalSource.TouchMove, _rrwebTypes.IncrementalSource.MediaInteraction, _rrwebTypes.IncrementalSource.Drag, _rrwebTypes.IncrementalSource.Selection]); + +function isUserEvent(ev) { + return ev.type === _rrwebTypes.EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); +} + +var MixpanelRecorder = function MixpanelRecorder(mixpanelInstance) { + this._mixpanel = mixpanelInstance; + + // internal rrweb stopRecording function + this._stopRecording = null; + + this.recEvents = []; + this.seqNo = 0; + this.replayId = null; + this.replayStartTime = null; + this.sendBatchId = null; + + this.idleTimeoutId = null; + this.maxTimeoutId = null; + + this.recordMaxMs = _utils.MAX_RECORDING_MS; + this._initBatcher(); +}; + +MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new _requestBatcher.RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _utils._.bind(this.flushEventsWithOptOut, this), + errorReporter: _utils._.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); +}; + +// eslint-disable-next-line camelcase +MixpanelRecorder.prototype.get_config = function (configVar) { + return this._mixpanel.get_config(configVar); +}; + +MixpanelRecorder.prototype.startRecording = function () { + if (this._stopRecording !== null) { + logger.log('Recording already in progress, skipping startRecording.'); + return; + } + + this.recordMaxMs = this.get_config('record_max_ms'); + if (this.recordMaxMs > _utils.MAX_RECORDING_MS) { + this.recordMaxMs = _utils.MAX_RECORDING_MS; + logger.critical('record_max_ms cannot be greater than ' + _utils.MAX_RECORDING_MS + 'ms. Capping value.'); + } + + this.recEvents = []; + this.seqNo = 0; + this.replayStartTime = null; + + this.replayId = _utils._.UUID(); + + this.batcher.start(); + + var resetIdleTimeout = _utils._.bind(function () { + clearTimeout(this.idleTimeoutId); + this.idleTimeoutId = setTimeout(_utils._.bind(function () { + logger.log('Idle timeout reached, restarting recording.'); + this.resetRecording(); + }, this), this.get_config('record_idle_timeout_ms')); + }, this); + + this._stopRecording = (0, _rrweb.record)({ + 'emit': _utils._.bind(function (ev) { + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } + }, this), + 'blockClass': this.get_config('record_block_class'), + 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, + 'maskTextClass': this.get_config('record_mask_text_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') + }); + + resetIdleTimeout(); + + this.maxTimeoutId = setTimeout(_utils._.bind(this.resetRecording, this), this.recordMaxMs); +}; + +MixpanelRecorder.prototype.resetRecording = function () { + this.stopRecording(); + this.startRecording(); +}; + +MixpanelRecorder.prototype.stopRecording = function () { + if (this._stopRecording !== null) { + this._stopRecording(); + this._stopRecording = null; + } + + this.batcher.flush(); // flush any remaining events + this.replayId = null; + + clearTimeout(this.idleTimeoutId); + clearTimeout(this.maxTimeoutId); +}; + +/** + * Flushes the current batch of events to the server, but passes an opt-out callback to make sure + * we stop recording and dump any queued events if the user has opted out. + */ +MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _utils._.bind(this._onOptOut, this)); +}; + +MixpanelRecorder.prototype._onOptOut = function (code) { + // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out + if (code === 0) { + this.recEvents = []; + this.stopRecording(); + } +}; + +MixpanelRecorder.prototype._sendRequest = function (reqParams, reqBody, callback) { + var onSuccess = _utils._.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + _utils.window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/octet-stream' + }, + 'body': reqBody + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + })['catch'](function (error) { + callback({ error: error }); + }); + })['catch'](function (error) { + callback({ error: error }); + }); +}; + +MixpanelRecorder.prototype._flushEvents = (0, _gdprUtils.addOptOutCheckMixpanelLib)(function (data, options, callback) { + var numEvents = data.length; + + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + + var reqParams = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + var eventsJson = _utils._.JSONEncode(data); + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqParams['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqParams['$user_id'] = userId; + } + + if (CompressionStream) { + var jsonStream = new Blob([eventsJson], { type: 'application/json' }).stream(); + var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); + new Response(gzipStream).blob().then(_utils._.bind(function (compressedBlob) { + reqParams['format'] = 'gzip'; + this._sendRequest(reqParams, compressedBlob, callback); + }, this)); + } else { + reqParams['format'] = 'body'; + this._sendRequest(reqParams, eventsJson, callback); + } + } +}); + +MixpanelRecorder.prototype.reportError = function (msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch (err) { + logger.error(err); + } +}; + +_utils.window['__mp_recorder'] = MixpanelRecorder; + +},{"../gdpr-utils":7,"../request-batcher":15,"../utils":18,"@rrweb/types":2,"rrweb":3}],15:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -4065,7 +15171,8 @@ var RequestBatcher = function RequestBatcher(storageKey, options) { this.errorReporter = options.errorReporter; this.queue = new _requestQueue.RequestQueue(storageKey, { errorReporter: _utils._.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -4082,6 +15189,11 @@ var RequestBatcher = function RequestBatcher(storageKey, options) { // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -4167,6 +15279,9 @@ RequestBatcher.prototype.flush = function (options) { var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _utils._.each(batch, function (item) { @@ -4224,20 +15339,16 @@ RequestBatcher.prototype.flush = function (options) { } else if (_utils._.isObject(res) && res.error === 'timeout' && new Date().getTime() - startTime >= timeoutMS) { this.reportError('Network timeout; retrying'); this.flush(); - } else if (_utils._.isObject(res) && res.xhr_req && (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')) { + } else if (_utils._.isObject(res) && (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = parseInt(retryAfter, 10) * 1000 || retryMS; - } + if (res.retryAfter) { + retryMS = parseInt(res.retryAfter, 10) * 1000 || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_utils._.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_utils._.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -4261,7 +15372,11 @@ RequestBatcher.prototype.flush = function (options) { }), _utils._.bind(function (succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -4332,7 +15447,7 @@ RequestBatcher.prototype.reportError = function (msg, err) { exports.RequestBatcher = RequestBatcher; -},{"./config":3,"./request-queue":12,"./utils":14}],12:[function(require,module,exports){ +},{"./config":5,"./request-queue":16,"./utils":18}],16:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -4370,6 +15485,7 @@ var RequestQueue = function RequestQueue(storageKey, options) { this.reportError = options.errorReporter || _utils._.bind(logger.error, logger); this.lock = new _sharedLock.SharedLock(storageKey, { storage: this.storage }); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -4394,29 +15510,36 @@ RequestQueue.prototype.enqueue = function (item, flushInterval, cb) { 'payload': item }; - this.lock.withLock(_utils._.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch (err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(succeeded); + cb(true); } - }, this), _utils._.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_utils._.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch (err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _utils._.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -4427,7 +15550,7 @@ RequestQueue.prototype.enqueue = function (item, flushInterval, cb) { */ RequestQueue.prototype.fillBatch = function (batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -4480,61 +15603,66 @@ RequestQueue.prototype.removeItemsByID = function (ids, cb) { }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _utils._.bind(function () { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _utils._.bind(function () { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch (err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch (err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _utils._.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!(0, _utils.localStorageSupported)(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch (err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _utils._.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!(0, _utils.localStorageSupported)(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch (err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } }; // internal helper for RequestQueue.updatePayloads @@ -4562,25 +15690,31 @@ var updatePayloads = function updatePayloads(existingItems, itemsToUpdate) { */ RequestQueue.prototype.updatePayloads = function (itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_utils._.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch (err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _utils._.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); + if (!this.usePersistence) { if (cb) { - cb(false); + cb(true); } - }, this), this.pid); + } else { + this.lock.withLock(_utils._.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch (err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _utils._.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -4623,12 +15757,15 @@ RequestQueue.prototype.saveToStorage = function (queue) { */ RequestQueue.prototype.clear = function () { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; exports.RequestQueue = RequestQueue; -},{"./shared-lock":13,"./utils":14}],13:[function(require,module,exports){ +},{"./shared-lock":17,"./utils":18}],17:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, '__esModule', { @@ -4788,7 +15925,7 @@ SharedLock.prototype.withLock = function (lockedCB, errorCB, pid) { exports.SharedLock = SharedLock; -},{"./utils":14}],14:[function(require,module,exports){ +},{"./utils":18}],18:[function(require,module,exports){ /* eslint camelcase: "off", eqeqeq: "off" */ 'use strict'; @@ -6517,4 +17654,4 @@ exports.JSONStringify = JSONStringify; exports.JSONParse = JSONParse; exports.slice = slice; -},{"./config":3}]},{},[1]); +},{"./config":5}]},{},[1]); diff --git a/examples/umd-webpack/bundle.js b/examples/umd-webpack/bundle.js index 942e026a..a56feacf 100644 --- a/examples/umd-webpack/bundle.js +++ b/examples/umd-webpack/bundle.js @@ -67,9 +67,4516 @@ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.mixpanel = factory()); })(this, (function () { 'use strict'; + var NodeType; + (function (NodeType) { + NodeType[NodeType["Document"] = 0] = "Document"; + NodeType[NodeType["DocumentType"] = 1] = "DocumentType"; + NodeType[NodeType["Element"] = 2] = "Element"; + NodeType[NodeType["Text"] = 3] = "Text"; + NodeType[NodeType["CDATA"] = 4] = "CDATA"; + NodeType[NodeType["Comment"] = 5] = "Comment"; + })(NodeType || (NodeType = {})); + + function isElement(n) { + return n.nodeType === n.ELEMENT_NODE; + } + function isShadowRoot(n) { + const host = n === null || n === void 0 ? void 0 : n.host; + return Boolean((host === null || host === void 0 ? void 0 : host.shadowRoot) === n); + } + function isNativeShadowDom(shadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; + } + function fixBrowserCompatibilityIssuesInCSS(cssText) { + if (cssText.includes(' background-clip: text;') && + !cssText.includes(' -webkit-background-clip: text;')) { + cssText = cssText.replace(' background-clip: text;', ' -webkit-background-clip: text; background-clip: text;'); + } + return cssText; + } + function escapeImportStatement(rule) { + const { cssText } = rule; + if (cssText.split('"').length < 3) + return cssText; + const statement = ['@import', `url(${JSON.stringify(rule.href)})`]; + if (rule.layerName === '') { + statement.push(`layer`); + } + else if (rule.layerName) { + statement.push(`layer(${rule.layerName})`); + } + if (rule.supportsText) { + statement.push(`supports(${rule.supportsText})`); + } + if (rule.media.length) { + statement.push(rule.media.mediaText); + } + return statement.join(' ') + ';'; + } + function stringifyStylesheet(s) { + try { + const rules = s.rules || s.cssRules; + return rules + ? fixBrowserCompatibilityIssuesInCSS(Array.from(rules, stringifyRule).join('')) + : null; + } + catch (error) { + return null; + } + } + function stringifyRule(rule) { + let importStringified; + if (isCSSImportRule(rule)) { + try { + importStringified = + stringifyStylesheet(rule.styleSheet) || + escapeImportStatement(rule); + } + catch (error) { + } + } + else if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) { + return fixSafariColons(rule.cssText); + } + return importStringified || rule.cssText; + } + function fixSafariColons(cssStringified) { + const regex = /(\[(?:[\w-]+)[^\\])(:(?:[\w-]+)\])/gm; + return cssStringified.replace(regex, '$1\\$2'); + } + function isCSSImportRule(rule) { + return 'styleSheet' in rule; + } + function isCSSStyleRule(rule) { + return 'selectorText' in rule; + } + class Mirror { + constructor() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + getId(n) { + var _a; + if (!n) + return -1; + const id = (_a = this.getMeta(n)) === null || _a === void 0 ? void 0 : _a.id; + return id !== null && id !== void 0 ? id : -1; + } + getNode(id) { + return this.idNodeMap.get(id) || null; + } + getIds() { + return Array.from(this.idNodeMap.keys()); + } + getMeta(n) { + return this.nodeMetaMap.get(n) || null; + } + removeNodeFromMap(n) { + const id = this.getId(n); + this.idNodeMap.delete(id); + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + } + } + has(id) { + return this.idNodeMap.has(id); + } + hasNode(node) { + return this.nodeMetaMap.has(node); + } + add(n, meta) { + const id = meta.id; + this.idNodeMap.set(id, n); + this.nodeMetaMap.set(n, meta); + } + replace(id, n) { + const oldNode = this.getNode(id); + if (oldNode) { + const meta = this.nodeMetaMap.get(oldNode); + if (meta) + this.nodeMetaMap.set(n, meta); + } + this.idNodeMap.set(id, n); + } + reset() { + this.idNodeMap = new Map(); + this.nodeMetaMap = new WeakMap(); + } + } + function createMirror() { + return new Mirror(); + } + function maskInputValue({ element, maskInputOptions, tagName, type, value, maskInputFn, }) { + let text = value || ''; + const actualType = type && toLowerCase(type); + if (maskInputOptions[tagName.toLowerCase()] || + (actualType && maskInputOptions[actualType])) { + if (maskInputFn) { + text = maskInputFn(text, element); + } + else { + text = '*'.repeat(text.length); + } + } + return text; + } + function toLowerCase(str) { + return str.toLowerCase(); + } + const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; + function is2DCanvasBlank(canvas) { + const ctx = canvas.getContext('2d'); + if (!ctx) + return true; + const chunkSize = 50; + for (let x = 0; x < canvas.width; x += chunkSize) { + for (let y = 0; y < canvas.height; y += chunkSize) { + const getImageData = ctx.getImageData; + const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData + ? getImageData[ORIGINAL_ATTRIBUTE_NAME] + : getImageData; + const pixelBuffer = new Uint32Array(originalGetImageData.call(ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y)).data.buffer); + if (pixelBuffer.some((pixel) => pixel !== 0)) + return false; + } + } + return true; + } + function getInputType(element) { + const type = element.type; + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? + toLowerCase(type) + : null; + } + function extractFileExtension(path, baseURL) { + var _a; + let url; + try { + url = new URL(path, baseURL !== null && baseURL !== void 0 ? baseURL : window.location.href); + } + catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; + const match = url.pathname.match(regex); + return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null; + } + + let _id = 1; + const tagNameRegex = new RegExp('[^a-z0-9-_:]'); + const IGNORED_NODE = -2; + function genId() { + return _id++; + } + function getValidTagName(element) { + if (element instanceof HTMLFormElement) { + return 'form'; + } + const processedTagName = toLowerCase(element.tagName); + if (tagNameRegex.test(processedTagName)) { + return 'div'; + } + return processedTagName; + } + function extractOrigin(url) { + let origin = ''; + if (url.indexOf('//') > -1) { + origin = url.split('/').slice(0, 3).join('/'); + } + else { + origin = url.split('/')[0]; + } + origin = origin.split('?')[0]; + return origin; + } + let canvasService; + let canvasCtx; + const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; + const URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\/\//i; + const URL_WWW_MATCH = /^www\..*/i; + const DATA_URI = /^(data:)([^,]*),(.*)/i; + function absoluteToStylesheet(cssText, href) { + return (cssText || '').replace(URL_IN_CSS_REF, (origin, quote1, path1, quote2, path2, path3) => { + const filePath = path1 || path2 || path3; + const maybeQuote = quote1 || quote2 || ''; + if (!filePath) { + return origin; + } + if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (DATA_URI.test(filePath)) { + return `url(${maybeQuote}${filePath}${maybeQuote})`; + } + if (filePath[0] === '/') { + return `url(${maybeQuote}${extractOrigin(href) + filePath}${maybeQuote})`; + } + const stack = href.split('/'); + const parts = filePath.split('/'); + stack.pop(); + for (const part of parts) { + if (part === '.') { + continue; + } + else if (part === '..') { + stack.pop(); + } + else { + stack.push(part); + } + } + return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`; + }); + } + const SRCSET_NOT_SPACES = /^[^ \t\n\r\u000c]+/; + const SRCSET_COMMAS_OR_SPACES = /^[, \t\n\r\u000c]+/; + function getAbsoluteSrcsetString(doc, attributeValue) { + if (attributeValue.trim() === '') { + return attributeValue; + } + let pos = 0; + function collectCharacters(regEx) { + let chars; + const match = regEx.exec(attributeValue.substring(pos)); + if (match) { + chars = match[0]; + pos += chars.length; + return chars; + } + return ''; + } + const output = []; + while (true) { + collectCharacters(SRCSET_COMMAS_OR_SPACES); + if (pos >= attributeValue.length) { + break; + } + let url = collectCharacters(SRCSET_NOT_SPACES); + if (url.slice(-1) === ',') { + url = absoluteToDoc(doc, url.substring(0, url.length - 1)); + output.push(url); + } + else { + let descriptorsStr = ''; + url = absoluteToDoc(doc, url); + let inParens = false; + while (true) { + const c = attributeValue.charAt(pos); + if (c === '') { + output.push((url + descriptorsStr).trim()); + break; + } + else if (!inParens) { + if (c === ',') { + pos += 1; + output.push((url + descriptorsStr).trim()); + break; + } + else if (c === '(') { + inParens = true; + } + } + else { + if (c === ')') { + inParens = false; + } + } + descriptorsStr += c; + pos += 1; + } + } + } + return output.join(', '); + } + function absoluteToDoc(doc, attributeValue) { + if (!attributeValue || attributeValue.trim() === '') { + return attributeValue; + } + const a = doc.createElement('a'); + a.href = attributeValue; + return a.href; + } + function isSVGElement(el) { + return Boolean(el.tagName === 'svg' || el.ownerSVGElement); + } + function getHref() { + const a = document.createElement('a'); + a.href = ''; + return a.href; + } + function transformAttribute(doc, tagName, name, value) { + if (!value) { + return value; + } + if (name === 'src' || + (name === 'href' && !(tagName === 'use' && value[0] === '#'))) { + return absoluteToDoc(doc, value); + } + else if (name === 'xlink:href' && value[0] !== '#') { + return absoluteToDoc(doc, value); + } + else if (name === 'background' && + (tagName === 'table' || tagName === 'td' || tagName === 'th')) { + return absoluteToDoc(doc, value); + } + else if (name === 'srcset') { + return getAbsoluteSrcsetString(doc, value); + } + else if (name === 'style') { + return absoluteToStylesheet(value, getHref()); + } + else if (tagName === 'object' && name === 'data') { + return absoluteToDoc(doc, value); + } + return value; + } + function ignoreAttribute(tagName, name, _value) { + return (tagName === 'video' || tagName === 'audio') && name === 'autoplay'; + } + function _isBlockedElement(element, blockClass, blockSelector) { + try { + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; + } + } + else { + for (let eIndex = element.classList.length; eIndex--;) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } + } + } + if (blockSelector) { + return element.matches(blockSelector); + } + } + catch (e) { + } + return false; + } + function classMatchesRegex(node, regex, checkAncestors) { + if (!node) + return false; + if (node.nodeType !== node.ELEMENT_NODE) { + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + for (let eIndex = node.classList.length; eIndex--;) { + const className = node.classList[eIndex]; + if (regex.test(className)) { + return true; + } + } + if (!checkAncestors) + return false; + return classMatchesRegex(node.parentNode, regex, checkAncestors); + } + function needMaskingText(node, maskTextClass, maskTextSelector, checkAncestors) { + try { + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + if (el === null) + return false; + if (typeof maskTextClass === 'string') { + if (checkAncestors) { + if (el.closest(`.${maskTextClass}`)) + return true; + } + else { + if (el.classList.contains(maskTextClass)) + return true; + } + } + else { + if (classMatchesRegex(el, maskTextClass, checkAncestors)) + return true; + } + if (maskTextSelector) { + if (checkAncestors) { + if (el.closest(maskTextSelector)) + return true; + } + else { + if (el.matches(maskTextSelector)) + return true; + } + } + } + catch (e) { + } + return false; + } + function onceIframeLoaded(iframeEl, listener, iframeLoadTimeout) { + const win = iframeEl.contentWindow; + if (!win) { + return; + } + let fired = false; + let readyState; + try { + readyState = win.document.readyState; + } + catch (error) { + return; + } + if (readyState !== 'complete') { + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, iframeLoadTimeout); + iframeEl.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + return; + } + const blankUrl = 'about:blank'; + if (win.location.href !== blankUrl || + iframeEl.src === blankUrl || + iframeEl.src === '') { + setTimeout(listener, 0); + return iframeEl.addEventListener('load', listener); + } + iframeEl.addEventListener('load', listener); + } + function onceStylesheetLoaded(link, listener, styleSheetLoadTimeout) { + let fired = false; + let styleSheetLoaded; + try { + styleSheetLoaded = link.sheet; + } + catch (error) { + return; + } + if (styleSheetLoaded) + return; + const timer = setTimeout(() => { + if (!fired) { + listener(); + fired = true; + } + }, styleSheetLoadTimeout); + link.addEventListener('load', () => { + clearTimeout(timer); + fired = true; + listener(); + }); + } + function serializeNode(n, options) { + const { doc, mirror, blockClass, blockSelector, needsMask, inlineStylesheet, maskInputOptions = {}, maskTextFn, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, } = options; + const rootId = getRootId(doc, mirror); + switch (n.nodeType) { + case n.DOCUMENT_NODE: + if (n.compatMode !== 'CSS1Compat') { + return { + type: NodeType.Document, + childNodes: [], + compatMode: n.compatMode, + }; + } + else { + return { + type: NodeType.Document, + childNodes: [], + }; + } + case n.DOCUMENT_TYPE_NODE: + return { + type: NodeType.DocumentType, + name: n.name, + publicId: n.publicId, + systemId: n.systemId, + rootId, + }; + case n.ELEMENT_NODE: + return serializeElementNode(n, { + doc, + blockClass, + blockSelector, + inlineStylesheet, + maskInputOptions, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + rootId, + }); + case n.TEXT_NODE: + return serializeTextNode(n, { + needsMask, + maskTextFn, + rootId, + }); + case n.CDATA_SECTION_NODE: + return { + type: NodeType.CDATA, + textContent: '', + rootId, + }; + case n.COMMENT_NODE: + return { + type: NodeType.Comment, + textContent: n.textContent || '', + rootId, + }; + default: + return false; + } + } + function getRootId(doc, mirror) { + if (!mirror.hasNode(doc)) + return undefined; + const docId = mirror.getId(doc); + return docId === 1 ? undefined : docId; + } + function serializeTextNode(n, options) { + var _a; + const { needsMask, maskTextFn, rootId } = options; + const parentTagName = n.parentNode && n.parentNode.tagName; + let textContent = n.textContent; + const isStyle = parentTagName === 'STYLE' ? true : undefined; + const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { + try { + if (n.nextSibling || n.previousSibling) { + } + else if ((_a = n.parentNode.sheet) === null || _a === void 0 ? void 0 : _a.cssRules) { + textContent = stringifyStylesheet(n.parentNode.sheet); + } + } + catch (err) { + console.warn(`Cannot get CSS styles from text's parentNode. Error: ${err}`, n); + } + textContent = absoluteToStylesheet(textContent, getHref()); + } + if (isScript) { + textContent = 'SCRIPT_PLACEHOLDER'; + } + if (!isStyle && !isScript && textContent && needsMask) { + textContent = maskTextFn + ? maskTextFn(textContent, n.parentElement) + : textContent.replace(/[\S]/g, '*'); + } + return { + type: NodeType.Text, + textContent: textContent || '', + isStyle, + rootId, + }; + } + function serializeElementNode(n, options) { + const { doc, blockClass, blockSelector, inlineStylesheet, maskInputOptions = {}, maskInputFn, dataURLOptions = {}, inlineImages, recordCanvas, keepIframeSrcFn, newlyAddedElement = false, rootId, } = options; + const needBlock = _isBlockedElement(n, blockClass, blockSelector); + const tagName = getValidTagName(n); + let attributes = {}; + const len = n.attributes.length; + for (let i = 0; i < len; i++) { + const attr = n.attributes[i]; + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute(doc, tagName, toLowerCase(attr.name), attr.value); + } + } + if (tagName === 'link' && inlineStylesheet) { + const stylesheet = Array.from(doc.styleSheets).find((s) => { + return s.href === n.href; + }); + let cssText = null; + if (stylesheet) { + cssText = stringifyStylesheet(stylesheet); + } + if (cssText) { + delete attributes.rel; + delete attributes.href; + attributes._cssText = absoluteToStylesheet(cssText, stylesheet.href); + } + } + if (tagName === 'style' && + n.sheet && + !(n.innerText || n.textContent || '').trim().length) { + const cssText = stringifyStylesheet(n.sheet); + if (cssText) { + attributes._cssText = absoluteToStylesheet(cssText, getHref()); + } + } + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + const value = n.value; + const checked = n.checked; + if (attributes.type !== 'radio' && + attributes.type !== 'checkbox' && + attributes.type !== 'submit' && + attributes.type !== 'button' && + value) { + attributes.value = maskInputValue({ + element: n, + type: getInputType(n), + tagName, + value, + maskInputOptions, + maskInputFn, + }); + } + else if (checked) { + attributes.checked = checked; + } + } + if (tagName === 'option') { + if (n.selected && !maskInputOptions['select']) { + attributes.selected = true; + } + else { + delete attributes.selected; + } + } + if (tagName === 'canvas' && recordCanvas) { + if (n.__context === '2d') { + if (!is2DCanvasBlank(n)) { + attributes.rr_dataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + } + else if (!('__context' in n)) { + const canvasDataURL = n.toDataURL(dataURLOptions.type, dataURLOptions.quality); + const blankCanvas = document.createElement('canvas'); + blankCanvas.width = n.width; + blankCanvas.height = n.height; + const blankCanvasDataURL = blankCanvas.toDataURL(dataURLOptions.type, dataURLOptions.quality); + if (canvasDataURL !== blankCanvasDataURL) { + attributes.rr_dataURL = canvasDataURL; + } + } + } + if (tagName === 'img' && inlineImages) { + if (!canvasService) { + canvasService = doc.createElement('canvas'); + canvasCtx = canvasService.getContext('2d'); + } + const image = n; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; + const recordInlineImage = () => { + image.removeEventListener('load', recordInlineImage); + try { + canvasService.width = image.naturalWidth; + canvasService.height = image.naturalHeight; + canvasCtx.drawImage(image, 0, 0); + attributes.rr_dataURL = canvasService.toDataURL(dataURLOptions.type, dataURLOptions.quality); + } + catch (err) { + console.warn(`Cannot inline img src=${image.currentSrc}! Error: ${err}`); + } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); + }; + if (image.complete && image.naturalWidth !== 0) + recordInlineImage(); + else + image.addEventListener('load', recordInlineImage); + } + if (tagName === 'audio' || tagName === 'video') { + const mediaAttributes = attributes; + mediaAttributes.rr_mediaState = n.paused + ? 'paused' + : 'played'; + mediaAttributes.rr_mediaCurrentTime = n.currentTime; + mediaAttributes.rr_mediaPlaybackRate = n.playbackRate; + mediaAttributes.rr_mediaMuted = n.muted; + mediaAttributes.rr_mediaLoop = n.loop; + mediaAttributes.rr_mediaVolume = n.volume; + } + if (!newlyAddedElement) { + if (n.scrollLeft) { + attributes.rr_scrollLeft = n.scrollLeft; + } + if (n.scrollTop) { + attributes.rr_scrollTop = n.scrollTop; + } + } + if (needBlock) { + const { width, height } = n.getBoundingClientRect(); + attributes = { + class: attributes.class, + rr_width: `${width}px`, + rr_height: `${height}px`, + }; + } + if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src)) { + if (!n.contentDocument) { + attributes.rr_src = attributes.src; + } + delete attributes.src; + } + let isCustomElement; + try { + if (customElements.get(tagName)) + isCustomElement = true; + } + catch (e) { + } + return { + type: NodeType.Element, + tagName, + attributes, + childNodes: [], + isSVG: isSVGElement(n) || undefined, + needBlock, + rootId, + isCustom: isCustomElement, + }; + } + function lowerIfExists(maybeAttr) { + if (maybeAttr === undefined || maybeAttr === null) { + return ''; + } + else { + return maybeAttr.toLowerCase(); + } + } + function slimDOMExcluded(sn, slimDOMOptions) { + if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + return true; + } + else if (sn.type === NodeType.Element) { + if (slimDOMOptions.script && + (sn.tagName === 'script' || + (sn.tagName === 'link' && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && + sn.attributes.as === 'script') || + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + extractFileExtension(sn.attributes.href) === 'js'))) { + return true; + } + else if (slimDOMOptions.headFavicon && + ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || + (sn.tagName === 'meta' && + (lowerIfExists(sn.attributes.name).match(/^msapplication-tile(image|color)$/) || + lowerIfExists(sn.attributes.name) === 'application-name' || + lowerIfExists(sn.attributes.rel) === 'icon' || + lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || + lowerIfExists(sn.attributes.rel) === 'shortcut icon')))) { + return true; + } + else if (sn.tagName === 'meta') { + if (slimDOMOptions.headMetaDescKeywords && + lowerIfExists(sn.attributes.name).match(/^description|keywords$/)) { + return true; + } + else if (slimDOMOptions.headMetaSocial && + (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || + lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || + lowerIfExists(sn.attributes.name) === 'pinterest')) { + return true; + } + else if (slimDOMOptions.headMetaRobots && + (lowerIfExists(sn.attributes.name) === 'robots' || + lowerIfExists(sn.attributes.name) === 'googlebot' || + lowerIfExists(sn.attributes.name) === 'bingbot')) { + return true; + } + else if (slimDOMOptions.headMetaHttpEquiv && + sn.attributes['http-equiv'] !== undefined) { + return true; + } + else if (slimDOMOptions.headMetaAuthorship && + (lowerIfExists(sn.attributes.name) === 'author' || + lowerIfExists(sn.attributes.name) === 'generator' || + lowerIfExists(sn.attributes.name) === 'framework' || + lowerIfExists(sn.attributes.name) === 'publisher' || + lowerIfExists(sn.attributes.name) === 'progid' || + lowerIfExists(sn.attributes.property).match(/^article:/) || + lowerIfExists(sn.attributes.property).match(/^product:/))) { + return true; + } + else if (slimDOMOptions.headMetaVerification && + (lowerIfExists(sn.attributes.name) === 'google-site-verification' || + lowerIfExists(sn.attributes.name) === 'yandex-verification' || + lowerIfExists(sn.attributes.name) === 'csrf-token' || + lowerIfExists(sn.attributes.name) === 'p:domain_verify' || + lowerIfExists(sn.attributes.name) === 'verify-v1' || + lowerIfExists(sn.attributes.name) === 'verification' || + lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token')) { + return true; + } + } + } + return false; + } + function serializeNodeWithId(n, options) { + const { doc, mirror, blockClass, blockSelector, maskTextClass, maskTextSelector, skipChild = false, inlineStylesheet = true, maskInputOptions = {}, maskTextFn, maskInputFn, slimDOMOptions, dataURLOptions = {}, inlineImages = false, recordCanvas = false, onSerialize, onIframeLoad, iframeLoadTimeout = 5000, onStylesheetLoad, stylesheetLoadTimeout = 5000, keepIframeSrcFn = () => false, newlyAddedElement = false, } = options; + let { needsMask } = options; + let { preserveWhiteSpace = true } = options; + if (!needsMask && + n.childNodes) { + const checkAncestors = needsMask === undefined; + needsMask = needMaskingText(n, maskTextClass, maskTextSelector, checkAncestors); + } + const _serializedNode = serializeNode(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + dataURLOptions, + inlineImages, + recordCanvas, + keepIframeSrcFn, + newlyAddedElement, + }); + if (!_serializedNode) { + console.warn(n, 'not serialized'); + return null; + } + let id; + if (mirror.hasNode(n)) { + id = mirror.getId(n); + } + else if (slimDOMExcluded(_serializedNode, slimDOMOptions) || + (!preserveWhiteSpace && + _serializedNode.type === NodeType.Text && + !_serializedNode.isStyle && + !_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)) { + id = IGNORED_NODE; + } + else { + id = genId(); + } + const serializedNode = Object.assign(_serializedNode, { id }); + mirror.add(n, serializedNode); + if (id === IGNORED_NODE) { + return null; + } + if (onSerialize) { + onSerialize(n); + } + let recordChild = !skipChild; + if (serializedNode.type === NodeType.Element) { + recordChild = recordChild && !serializedNode.needBlock; + delete serializedNode.needBlock; + const shadowRoot = n.shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; + } + if ((serializedNode.type === NodeType.Document || + serializedNode.type === NodeType.Element) && + recordChild) { + if (slimDOMOptions.headWhitespace && + serializedNode.type === NodeType.Element && + serializedNode.tagName === 'head') { + preserveWhiteSpace = false; + } + const bypassOptions = { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }; + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'textarea' && + serializedNode.attributes.value !== undefined) ; + else { + for (const childN of Array.from(n.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + serializedNode.childNodes.push(serializedChildNode); + } + } + } + if (isElement(n) && n.shadowRoot) { + for (const childN of Array.from(n.shadowRoot.childNodes)) { + const serializedChildNode = serializeNodeWithId(childN, bypassOptions); + if (serializedChildNode) { + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); + serializedNode.childNodes.push(serializedChildNode); + } + } + } + } + if (n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode)) { + serializedNode.isShadow = true; + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'iframe') { + onceIframeLoaded(n, () => { + const iframeDoc = n.contentDocument; + if (iframeDoc && onIframeLoad) { + const serializedIframeNode = serializeNodeWithId(iframeDoc, { + doc: iframeDoc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedIframeNode) { + onIframeLoad(n, serializedIframeNode); + } + } + }, iframeLoadTimeout); + } + if (serializedNode.type === NodeType.Element && + serializedNode.tagName === 'link' && + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css'))) { + onceStylesheetLoaded(n, () => { + if (onStylesheetLoad) { + const serializedLinkNode = serializeNodeWithId(n, { + doc, + mirror, + blockClass, + blockSelector, + needsMask, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + }); + if (serializedLinkNode) { + onStylesheetLoad(n, serializedLinkNode); + } + } + }, stylesheetLoadTimeout); + } + return serializedNode; + } + function snapshot(n, options) { + const { mirror = new Mirror(), blockClass = 'rr-block', blockSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, inlineImages = false, recordCanvas = false, maskAllInputs = false, maskTextFn, maskInputFn, slimDOM = false, dataURLOptions, preserveWhiteSpace, onSerialize, onIframeLoad, iframeLoadTimeout, onStylesheetLoad, stylesheetLoadTimeout, keepIframeSrcFn = () => false, } = options || {}; + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : maskAllInputs === false + ? { + password: true, + } + : maskAllInputs; + const slimDOMOptions = slimDOM === true || slimDOM === 'all' + ? + { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaDescKeywords: slimDOM === 'all', + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaAuthorship: true, + headMetaVerification: true, + } + : slimDOM === false + ? {} + : slimDOM; + return serializeNodeWithId(n, { + doc: n, + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + skipChild: false, + inlineStylesheet, + maskInputOptions, + maskTextFn, + maskInputFn, + slimDOMOptions, + dataURLOptions, + inlineImages, + recordCanvas, + preserveWhiteSpace, + onSerialize, + onIframeLoad, + iframeLoadTimeout, + onStylesheetLoad, + stylesheetLoadTimeout, + keepIframeSrcFn, + newlyAddedElement: false, + }); + } + + function on(type, fn, target = document) { + const options = { capture: true, passive: true }; + target.addEventListener(type, fn, options); + return () => target.removeEventListener(type, fn, options); + } + const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + + '\r\n' + + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + + '\r\n' + + 'or you can use record.mirror to access the mirror instance during recording.'; + let _mirror = { + map: {}, + getId() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return -1; + }, + getNode() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return null; + }, + removeNodeFromMap() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + has() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + return false; + }, + reset() { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + }, + }; + if (typeof window !== 'undefined' && window.Proxy && window.Reflect) { + _mirror = new Proxy(_mirror, { + get(target, prop, receiver) { + if (prop === 'map') { + console.error(DEPARTED_MIRROR_ACCESS_WARNING); + } + return Reflect.get(target, prop, receiver); + }, + }); + } + function throttle(func, wait, options = {}) { + let timeout = null; + let previous = 0; + return function (...args) { + const now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + const context = this; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } + else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; + } + function hookSetter(target, key, d, isRevoked, win = window) { + const original = win.Object.getOwnPropertyDescriptor(target, key); + win.Object.defineProperty(target, key, isRevoked + ? d + : { + set(value) { + setTimeout(() => { + d.set.call(this, value); + }, 0); + if (original && original.set) { + original.set.call(this, value); + } + }, + }); + return () => hookSetter(target, key, original || {}, true); + } + function patch(source, name, replacement) { + try { + if (!(name in source)) { + return () => { + }; + } + const original = source[name]; + const wrapped = replacement(original); + if (typeof wrapped === 'function') { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __rrweb_original__: { + enumerable: false, + value: original, + }, + }); + } + source[name] = wrapped; + return () => { + source[name] = original; + }; + } + catch (_a) { + return () => { + }; + } + } + let nowTimestamp = Date.now; + if (!(/[1-9][0-9]{12}/.test(Date.now().toString()))) { + nowTimestamp = () => new Date().getTime(); + } + function getWindowScroll(win) { + var _a, _b, _c, _d, _e, _f; + const doc = win.document; + return { + left: doc.scrollingElement + ? doc.scrollingElement.scrollLeft + : win.pageXOffset !== undefined + ? win.pageXOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollLeft) || + ((_b = (_a = doc === null || doc === void 0 ? void 0 : doc.body) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.scrollLeft) || + ((_c = doc === null || doc === void 0 ? void 0 : doc.body) === null || _c === void 0 ? void 0 : _c.scrollLeft) || + 0, + top: doc.scrollingElement + ? doc.scrollingElement.scrollTop + : win.pageYOffset !== undefined + ? win.pageYOffset + : (doc === null || doc === void 0 ? void 0 : doc.documentElement.scrollTop) || + ((_e = (_d = doc === null || doc === void 0 ? void 0 : doc.body) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.scrollTop) || + ((_f = doc === null || doc === void 0 ? void 0 : doc.body) === null || _f === void 0 ? void 0 : _f.scrollTop) || + 0, + }; + } + function getWindowHeight() { + return (window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight)); + } + function getWindowWidth() { + return (window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth)); + } + function closestElementOfNode(node) { + if (!node) { + return null; + } + const el = node.nodeType === node.ELEMENT_NODE + ? node + : node.parentElement; + return el; + } + function isBlocked(node, blockClass, blockSelector, checkAncestors) { + if (!node) { + return false; + } + const el = closestElementOfNode(node); + if (!el) { + return false; + } + try { + if (typeof blockClass === 'string') { + if (el.classList.contains(blockClass)) + return true; + if (checkAncestors && el.closest('.' + blockClass) !== null) + return true; + } + else { + if (classMatchesRegex(el, blockClass, checkAncestors)) + return true; + } + } + catch (e) { + } + if (blockSelector) { + if (el.matches(blockSelector)) + return true; + if (checkAncestors && el.closest(blockSelector) !== null) + return true; + } + return false; + } + function isSerialized(n, mirror) { + return mirror.getId(n) !== -1; + } + function isIgnored(n, mirror) { + return mirror.getId(n) === IGNORED_NODE; + } + function isAncestorRemoved(target, mirror) { + if (isShadowRoot(target)) { + return false; + } + const id = mirror.getId(target); + if (!mirror.has(id)) { + return true; + } + if (target.parentNode && + target.parentNode.nodeType === target.DOCUMENT_NODE) { + return false; + } + if (!target.parentNode) { + return true; + } + return isAncestorRemoved(target.parentNode, mirror); + } + function legacy_isTouchEvent(event) { + return Boolean(event.changedTouches); + } + function polyfill(win = window) { + if ('NodeList' in win && !win.NodeList.prototype.forEach) { + win.NodeList.prototype.forEach = Array.prototype + .forEach; + } + if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach; + } + if (!Node.prototype.contains) { + Node.prototype.contains = (...args) => { + let node = args[0]; + if (!(0 in args)) { + throw new TypeError('1 argument is required'); + } + do { + if (this === node) { + return true; + } + } while ((node = node && node.parentNode)); + return false; + }; + } + } + function isSerializedIframe(n, mirror) { + return Boolean(n.nodeName === 'IFRAME' && mirror.getMeta(n)); + } + function isSerializedStylesheet(n, mirror) { + return Boolean(n.nodeName === 'LINK' && + n.nodeType === n.ELEMENT_NODE && + n.getAttribute && + n.getAttribute('rel') === 'stylesheet' && + mirror.getMeta(n)); + } + function hasShadowRoot(n) { + return Boolean(n === null || n === void 0 ? void 0 : n.shadowRoot); + } + class StyleSheetMirror { + constructor() { + this.id = 1; + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + } + getId(stylesheet) { + var _a; + return (_a = this.styleIDMap.get(stylesheet)) !== null && _a !== void 0 ? _a : -1; + } + has(stylesheet) { + return this.styleIDMap.has(stylesheet); + } + add(stylesheet, id) { + if (this.has(stylesheet)) + return this.getId(stylesheet); + let newId; + if (id === undefined) { + newId = this.id++; + } + else + newId = id; + this.styleIDMap.set(stylesheet, newId); + this.idStyleMap.set(newId, stylesheet); + return newId; + } + getStyle(id) { + return this.idStyleMap.get(id) || null; + } + reset() { + this.styleIDMap = new WeakMap(); + this.idStyleMap = new Map(); + this.id = 1; + } + generateId() { + return this.id++; + } + } + function getShadowHost(n) { + var _a, _b; + let shadowHost = null; + if (((_b = (_a = n.getRootNode) === null || _a === void 0 ? void 0 : _a.call(n)) === null || _b === void 0 ? void 0 : _b.nodeType) === Node.DOCUMENT_FRAGMENT_NODE && + n.getRootNode().host) + shadowHost = n.getRootNode().host; + return shadowHost; + } + function getRootShadowHost(n) { + let rootShadowHost = n; + let shadowHost; + while ((shadowHost = getShadowHost(rootShadowHost))) + rootShadowHost = shadowHost; + return rootShadowHost; + } + function shadowHostInDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + const shadowHost = getRootShadowHost(n); + return doc.contains(shadowHost); + } + function inDom(n) { + const doc = n.ownerDocument; + if (!doc) + return false; + return doc.contains(n) || shadowHostInDom(n); + } + + var EventType$1 = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType$1 || {}); + var IncrementalSource$1 = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource$1 || {}); + var MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => { + MouseInteractions2[MouseInteractions2["MouseUp"] = 0] = "MouseUp"; + MouseInteractions2[MouseInteractions2["MouseDown"] = 1] = "MouseDown"; + MouseInteractions2[MouseInteractions2["Click"] = 2] = "Click"; + MouseInteractions2[MouseInteractions2["ContextMenu"] = 3] = "ContextMenu"; + MouseInteractions2[MouseInteractions2["DblClick"] = 4] = "DblClick"; + MouseInteractions2[MouseInteractions2["Focus"] = 5] = "Focus"; + MouseInteractions2[MouseInteractions2["Blur"] = 6] = "Blur"; + MouseInteractions2[MouseInteractions2["TouchStart"] = 7] = "TouchStart"; + MouseInteractions2[MouseInteractions2["TouchMove_Departed"] = 8] = "TouchMove_Departed"; + MouseInteractions2[MouseInteractions2["TouchEnd"] = 9] = "TouchEnd"; + MouseInteractions2[MouseInteractions2["TouchCancel"] = 10] = "TouchCancel"; + return MouseInteractions2; + })(MouseInteractions || {}); + var PointerTypes = /* @__PURE__ */ ((PointerTypes2) => { + PointerTypes2[PointerTypes2["Mouse"] = 0] = "Mouse"; + PointerTypes2[PointerTypes2["Pen"] = 1] = "Pen"; + PointerTypes2[PointerTypes2["Touch"] = 2] = "Touch"; + return PointerTypes2; + })(PointerTypes || {}); + var CanvasContext = /* @__PURE__ */ ((CanvasContext2) => { + CanvasContext2[CanvasContext2["2D"] = 0] = "2D"; + CanvasContext2[CanvasContext2["WebGL"] = 1] = "WebGL"; + CanvasContext2[CanvasContext2["WebGL2"] = 2] = "WebGL2"; + return CanvasContext2; + })(CanvasContext || {}); + + function isNodeInLinkedList(n) { + return '__ln' in n; + } + class DoubleLinkedList { + constructor() { + this.length = 0; + this.head = null; + this.tail = null; + } + get(position) { + if (position >= this.length) { + throw new Error('Position outside of list range'); + } + let current = this.head; + for (let index = 0; index < position; index++) { + current = (current === null || current === void 0 ? void 0 : current.next) || null; + } + return current; + } + addNode(n) { + const node = { + value: n, + previous: null, + next: null, + }; + n.__ln = node; + if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) { + const current = n.previousSibling.__ln.next; + node.next = current; + node.previous = n.previousSibling.__ln; + n.previousSibling.__ln.next = node; + if (current) { + current.previous = node; + } + } + else if (n.nextSibling && + isNodeInLinkedList(n.nextSibling) && + n.nextSibling.__ln.previous) { + const current = n.nextSibling.__ln.previous; + node.previous = current; + node.next = n.nextSibling.__ln; + n.nextSibling.__ln.previous = node; + if (current) { + current.next = node; + } + } + else { + if (this.head) { + this.head.previous = node; + } + node.next = this.head; + this.head = node; + } + if (node.next === null) { + this.tail = node; + } + this.length++; + } + removeNode(n) { + const current = n.__ln; + if (!this.head) { + return; + } + if (!current.previous) { + this.head = current.next; + if (this.head) { + this.head.previous = null; + } + else { + this.tail = null; + } + } + else { + current.previous.next = current.next; + if (current.next) { + current.next.previous = current.previous; + } + else { + this.tail = current.previous; + } + } + if (n.__ln) { + delete n.__ln; + } + this.length--; + } + } + const moveKey = (id, parentId) => `${id}@${parentId}`; + class MutationBuffer { + constructor() { + this.frozen = false; + this.locked = false; + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.mapRemoves = []; + this.movedMap = {}; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.processMutations = (mutations) => { + mutations.forEach(this.processMutation); + this.emit(); + }; + this.emit = () => { + if (this.frozen || this.locked) { + return; + } + const adds = []; + const addedIds = new Set(); + const addList = new DoubleLinkedList(); + const getNextId = (n) => { + let ns = n; + let nextId = IGNORED_NODE; + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && this.mirror.getId(ns); + } + return nextId; + }; + const pushAdd = (n) => { + if (!n.parentNode || + !inDom(n) || + n.parentNode.tagName === 'TEXTAREA') { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement(currentN); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } + }; + while (this.mapRemoves.length) { + this.mirror.removeNodeFromMap(this.mapRemoves.shift()); + } + for (const n of this.movedSet) { + if (isParentRemoved(this.removes, n, this.mirror) && + !this.movedSet.has(n.parentNode)) { + continue; + } + pushAdd(n); + } + for (const n of this.addedSet) { + if (!isAncestorInSet(this.droppedSet, n) && + !isParentRemoved(this.removes, n, this.mirror)) { + pushAdd(n); + } + else if (isAncestorInSet(this.movedSet, n)) { + pushAdd(n); + } + else { + this.droppedSet.add(n); + } + } + let candidate = null; + while (addList.length) { + let node = null; + if (candidate) { + const parentId = this.mirror.getId(candidate.value.parentNode); + const nextId = getNextId(candidate.value); + if (parentId !== -1 && nextId !== -1) { + node = candidate; + } + } + if (!node) { + let tailNode = addList.tail; + while (tailNode) { + const _node = tailNode; + tailNode = tailNode.previous; + if (_node) { + const parentId = this.mirror.getId(_node.value.parentNode); + const nextId = getNextId(_node.value); + if (nextId === -1) + continue; + else if (parentId !== -1) { + node = _node; + break; + } + else { + const unhandledNode = _node.value; + if (unhandledNode.parentNode && + unhandledNode.parentNode.nodeType === + Node.DOCUMENT_FRAGMENT_NODE) { + const shadowHost = unhandledNode.parentNode + .host; + const parentId = this.mirror.getId(shadowHost); + if (parentId !== -1) { + node = _node; + break; + } + } + } + } + } + } + if (!node) { + while (addList.head) { + addList.removeNode(addList.head.value); + } + break; + } + candidate = node.previous; + addList.removeNode(node.value); + pushAdd(node.value); + } + const payload = { + texts: this.texts + .map((text) => { + const n = text.node; + if (n.parentNode && + n.parentNode.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(n.parentNode); + } + return { + id: this.mirror.getId(n), + value: text.value, + }; + }) + .filter((text) => !addedIds.has(text.id)) + .filter((text) => this.mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => { + const { attributes } = attribute; + if (typeof attributes.style === 'string') { + const diffAsStr = JSON.stringify(attribute.styleDiff); + const unchangedAsStr = JSON.stringify(attribute._unchangedStyles); + if (diffAsStr.length < attributes.style.length) { + if ((diffAsStr + unchangedAsStr).split('var(').length === + attributes.style.split('var(').length) { + attributes.style = attribute.styleDiff; + } + } + } + return { + id: this.mirror.getId(attribute.node), + attributes: attributes, + }; + }) + .filter((attribute) => !addedIds.has(attribute.id)) + .filter((attribute) => this.mirror.has(attribute.id)), + removes: this.removes, + adds, + }; + if (!payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length) { + return; + } + this.texts = []; + this.attributes = []; + this.attributeMap = new WeakMap(); + this.removes = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + this.mutationCb(payload); + }; + this.genTextAreaValueMutation = (textarea) => { + let item = this.attributeMap.get(textarea); + if (!item) { + item = { + node: textarea, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(textarea, item); + } + item.attributes.value = Array.from(textarea.childNodes, (cn) => cn.textContent || '').join(''); + }; + this.processMutation = (m) => { + if (isIgnored(m.target, this.mirror)) { + return; + } + switch (m.type) { + case 'characterData': { + const value = m.target.textContent; + if (!isBlocked(m.target, this.blockClass, this.blockSelector, false) && + value !== m.oldValue) { + this.texts.push({ + value: needMaskingText(m.target, this.maskTextClass, this.maskTextSelector, true) && value + ? this.maskTextFn + ? this.maskTextFn(value, closestElementOfNode(m.target)) + : value.replace(/[\S]/g, '*') + : value, + node: m.target, + }); + } + break; + } + case 'attributes': { + const target = m.target; + let attributeName = m.attributeName; + let value = m.target.getAttribute(attributeName); + if (attributeName === 'value') { + const type = getInputType(target); + value = maskInputValue({ + element: target, + maskInputOptions: this.maskInputOptions, + tagName: target.tagName, + type, + value, + maskInputFn: this.maskInputFn, + }); + } + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + value === m.oldValue) { + return; + } + let item = this.attributeMap.get(m.target); + if (target.tagName === 'IFRAME' && + attributeName === 'src' && + !this.keepIframeSrcFn(value)) { + if (!target.contentDocument) { + attributeName = 'rr_src'; + } + else { + return; + } + } + if (!item) { + item = { + node: m.target, + attributes: {}, + styleDiff: {}, + _unchangedStyles: {}, + }; + this.attributes.push(item); + this.attributeMap.set(m.target, item); + } + if (attributeName === 'type' && + target.tagName === 'INPUT' && + (m.oldValue || '').toLowerCase() === 'password') { + target.setAttribute('data-rr-is-password', 'true'); + } + if (!ignoreAttribute(target.tagName, attributeName)) { + item.attributes[attributeName] = transformAttribute(this.doc, toLowerCase(target.tagName), toLowerCase(attributeName), value); + if (attributeName === 'style') { + if (!this.unattachedDoc) { + try { + this.unattachedDoc = + document.implementation.createHTMLDocument(); + } + catch (e) { + this.unattachedDoc = this.doc; + } + } + const old = this.unattachedDoc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if (newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname)) { + if (newPriority === '') { + item.styleDiff[pname] = newValue; + } + else { + item.styleDiff[pname] = [newValue, newPriority]; + } + } + else { + item._unchangedStyles[pname] = [newValue, newPriority]; + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + item.styleDiff[pname] = false; + } + } + } + } + break; + } + case 'childList': { + if (isBlocked(m.target, this.blockClass, this.blockSelector, true)) + return; + if (m.target.tagName === 'TEXTAREA') { + this.genTextAreaValueMutation(m.target); + return; + } + m.addedNodes.forEach((n) => this.genAdds(n, m.target)); + m.removedNodes.forEach((n) => { + const nodeId = this.mirror.getId(n); + const parentId = isShadowRoot(m.target) + ? this.mirror.getId(m.target.host) + : this.mirror.getId(m.target); + if (isBlocked(m.target, this.blockClass, this.blockSelector, false) || + isIgnored(n, this.mirror) || + !isSerialized(n, this.mirror)) { + return; + } + if (this.addedSet.has(n)) { + deepDelete(this.addedSet, n); + this.droppedSet.add(n); + } + else if (this.addedSet.has(m.target) && nodeId === -1) ; + else if (isAncestorRemoved(m.target, this.mirror)) ; + else if (this.movedSet.has(n) && + this.movedMap[moveKey(nodeId, parentId)]) { + deepDelete(this.movedSet, n); + } + else { + this.removes.push({ + parentId, + id: nodeId, + isShadow: isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, + }); + } + this.mapRemoves.push(n); + }); + break; + } + } + }; + this.genAdds = (n, target) => { + if (this.processedNodeManager.inOtherBuffer(n, this)) + return; + if (this.addedSet.has(n) || this.movedSet.has(n)) + return; + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + return; + } + this.movedSet.add(n); + let targetId = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } + else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); + } + } + }; + } + init(options) { + [ + 'mutationCb', + 'blockClass', + 'blockSelector', + 'maskTextClass', + 'maskTextSelector', + 'inlineStylesheet', + 'maskInputOptions', + 'maskTextFn', + 'maskInputFn', + 'keepIframeSrcFn', + 'recordCanvas', + 'inlineImages', + 'slimDOMOptions', + 'dataURLOptions', + 'doc', + 'mirror', + 'iframeManager', + 'stylesheetManager', + 'shadowDomManager', + 'canvasManager', + 'processedNodeManager', + ].forEach((key) => { + this[key] = options[key]; + }); + } + freeze() { + this.frozen = true; + this.canvasManager.freeze(); + } + unfreeze() { + this.frozen = false; + this.canvasManager.unfreeze(); + this.emit(); + } + isFrozen() { + return this.frozen; + } + lock() { + this.locked = true; + this.canvasManager.lock(); + } + unlock() { + this.locked = false; + this.canvasManager.unlock(); + this.emit(); + } + reset() { + this.shadowDomManager.reset(); + this.canvasManager.reset(); + } + } + function deepDelete(addsSet, n) { + addsSet.delete(n); + n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); + } + function isParentRemoved(removes, n, mirror) { + if (removes.length === 0) + return false; + return _isParentRemoved(removes, n, mirror); + } + function _isParentRemoved(removes, n, mirror) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + const parentId = mirror.getId(parentNode); + if (removes.some((r) => r.id === parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror); + } + function isAncestorInSet(set, n) { + if (set.size === 0) + return false; + return _isAncestorInSet(set, n); + } + function _isAncestorInSet(set, n) { + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + return _isAncestorInSet(set, parentNode); + } + + let errorHandler; + function registerErrorHandler(handler) { + errorHandler = handler; + } + function unregisterErrorHandler() { + errorHandler = undefined; + } + const callbackWrapper = (cb) => { + if (!errorHandler) { + return cb; + } + const rrwebWrapped = ((...rest) => { + try { + return cb(...rest); + } + catch (error) { + if (errorHandler && errorHandler(error) === true) { + return; + } + throw error; + } + }); + return rrwebWrapped; + }; + + const mutationBuffers = []; + function getEventTarget(event) { + try { + if ('composedPath' in event) { + const path = event.composedPath(); + if (path.length) { + return path[0]; + } + } + else if ('path' in event && event.path.length) { + return event.path[0]; + } + } + catch (_a) { + } + return event && event.target; + } + function initMutationObserver(options, rootEl) { + var _a, _b; + const mutationBuffer = new MutationBuffer(); + mutationBuffers.push(mutationBuffer); + mutationBuffer.init(options); + let mutationObserverCtor = window.MutationObserver || + window.__rrMutationObserver; + const angularZoneSymbol = (_b = (_a = window === null || window === void 0 ? void 0 : window.Zone) === null || _a === void 0 ? void 0 : _a.__symbol__) === null || _b === void 0 ? void 0 : _b.call(_a, 'MutationObserver'); + if (angularZoneSymbol && + window[angularZoneSymbol]) { + mutationObserverCtor = window[angularZoneSymbol]; + } + const observer = new mutationObserverCtor(callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer))); + observer.observe(rootEl, { + attributes: true, + attributeOldValue: true, + characterData: true, + characterDataOldValue: true, + childList: true, + subtree: true, + }); + return observer; + } + function initMoveObserver({ mousemoveCb, sampling, doc, mirror, }) { + if (sampling.mousemove === false) { + return () => { + }; + } + const threshold = typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + const callbackThreshold = typeof sampling.mousemoveCallback === 'number' + ? sampling.mousemoveCallback + : 500; + let positions = []; + let timeBaseline; + const wrappedCb = throttle(callbackWrapper((source) => { + const totalOffset = Date.now() - timeBaseline; + mousemoveCb(positions.map((p) => { + p.timeOffset -= totalOffset; + return p; + }), source); + positions = []; + timeBaseline = null; + }), callbackThreshold); + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + const { clientX, clientY } = legacy_isTouchEvent(evt) + ? evt.changedTouches[0] + : evt; + if (!timeBaseline) { + timeBaseline = nowTimestamp(); + } + positions.push({ + x: clientX, + y: clientY, + id: mirror.getId(target), + timeOffset: nowTimestamp() - timeBaseline, + }); + wrappedCb(typeof DragEvent !== 'undefined' && evt instanceof DragEvent + ? IncrementalSource$1.Drag + : evt instanceof MouseEvent + ? IncrementalSource$1.MouseMove + : IncrementalSource$1.TouchMove); + }), threshold, { + trailing: false, + })); + const handlers = [ + on('mousemove', updatePosition, doc), + on('touchmove', updatePosition, doc), + on('drag', updatePosition, doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initMouseInteractionObserver({ mouseInteractionCb, doc, mirror, blockClass, blockSelector, sampling, }) { + if (sampling.mouseInteraction === false) { + return () => { + }; + } + const disableMap = sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers = []; + let currentPointerType = null; + const getHandler = (eventKey) => { + return (event) => { + const target = getEventTarget(event); + if (isBlocked(target, blockClass, blockSelector, true)) { + return; + } + let pointerType = null; + let thisEventKey = eventKey; + if ('pointerType' in event) { + switch (event.pointerType) { + case 'mouse': + pointerType = PointerTypes.Mouse; + break; + case 'touch': + pointerType = PointerTypes.Touch; + break; + case 'pen': + pointerType = PointerTypes.Pen; + break; + } + if (pointerType === PointerTypes.Touch) { + if (MouseInteractions[eventKey] === MouseInteractions.MouseDown) { + thisEventKey = 'TouchStart'; + } + else if (MouseInteractions[eventKey] === MouseInteractions.MouseUp) { + thisEventKey = 'TouchEnd'; + } + } + else if (pointerType === PointerTypes.Pen) ; + } + else if (legacy_isTouchEvent(event)) { + pointerType = PointerTypes.Touch; + } + if (pointerType !== null) { + currentPointerType = pointerType; + if ((thisEventKey.startsWith('Touch') && + pointerType === PointerTypes.Touch) || + (thisEventKey.startsWith('Mouse') && + pointerType === PointerTypes.Mouse)) { + pointerType = null; + } + } + else if (MouseInteractions[eventKey] === MouseInteractions.Click) { + pointerType = currentPointerType; + currentPointerType = null; + } + const e = legacy_isTouchEvent(event) ? event.changedTouches[0] : event; + if (!e) { + return; + } + const id = mirror.getId(target); + const { clientX, clientY } = e; + callbackWrapper(mouseInteractionCb)(Object.assign({ type: MouseInteractions[thisEventKey], id, x: clientX, y: clientY }, (pointerType !== null && { pointerType }))); + }; + }; + Object.keys(MouseInteractions) + .filter((key) => Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false) + .forEach((eventKey) => { + let eventName = toLowerCase(eventKey); + const handler = getHandler(eventKey); + if (window.PointerEvent) { + switch (MouseInteractions[eventKey]) { + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + eventName = eventName.replace('mouse', 'pointer'); + break; + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + return; + } + } + handlers.push(on(eventName, handler, doc)); + }); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }) { + const updatePosition = callbackWrapper(throttle(callbackWrapper((evt) => { + const target = getEventTarget(evt); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const id = mirror.getId(target); + if (target === doc && doc.defaultView) { + const scrollLeftTop = getWindowScroll(doc.defaultView); + scrollCb({ + id, + x: scrollLeftTop.left, + y: scrollLeftTop.top, + }); + } + else { + scrollCb({ + id, + x: target.scrollLeft, + y: target.scrollTop, + }); + } + }), sampling.scroll || 100)); + return on('scroll', updatePosition, doc); + } + function initViewportResizeObserver({ viewportResizeCb }, { win }) { + let lastH = -1; + let lastW = -1; + const updateDimension = callbackWrapper(throttle(callbackWrapper(() => { + const height = getWindowHeight(); + const width = getWindowWidth(); + if (lastH !== height || lastW !== width) { + viewportResizeCb({ + width: Number(width), + height: Number(height), + }); + lastH = height; + lastW = width; + } + }), 200)); + return on('resize', updateDimension, win); + } + const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; + const lastInputValueMap = new WeakMap(); + function initInputObserver({ inputCb, doc, mirror, blockClass, blockSelector, ignoreClass, ignoreSelector, maskInputOptions, maskInputFn, sampling, userTriggeredOnInput, }) { + function eventHandler(event) { + let target = getEventTarget(event); + const userTriggered = event.isTrusted; + const tagName = target && target.tagName; + if (target && tagName === 'OPTION') { + target = target.parentElement; + } + if (!target || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + if (target.classList.contains(ignoreClass) || + (ignoreSelector && target.matches(ignoreSelector))) { + return; + } + let text = target.value; + let isChecked = false; + const type = getInputType(target) || ''; + if (type === 'radio' || type === 'checkbox') { + isChecked = target.checked; + } + else if (maskInputOptions[tagName.toLowerCase()] || + maskInputOptions[type]) { + text = maskInputValue({ + element: target, + maskInputOptions, + tagName, + type, + value: text, + maskInputFn, + }); + } + cbWithDedup(target, userTriggeredOnInput + ? { text, isChecked, userTriggered } + : { text, isChecked }); + const name = target.name; + if (type === 'radio' && name && isChecked) { + doc + .querySelectorAll(`input[type="radio"][name="${name}"]`) + .forEach((el) => { + if (el !== target) { + const text = el.value; + cbWithDedup(el, userTriggeredOnInput + ? { text, isChecked: !isChecked, userTriggered: false } + : { text, isChecked: !isChecked }); + } + }); + } + } + function cbWithDedup(target, v) { + const lastInputValue = lastInputValueMap.get(target); + if (!lastInputValue || + lastInputValue.text !== v.text || + lastInputValue.isChecked !== v.isChecked) { + lastInputValueMap.set(target, v); + const id = mirror.getId(target); + callbackWrapper(inputCb)(Object.assign(Object.assign({}, v), { id })); + } + } + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers = events.map((eventName) => on(eventName, callbackWrapper(eventHandler), doc)); + const currentWindow = doc.defaultView; + if (!currentWindow) { + return () => { + handlers.forEach((h) => h()); + }; + } + const propertyDescriptor = currentWindow.Object.getOwnPropertyDescriptor(currentWindow.HTMLInputElement.prototype, 'value'); + const hookProperties = [ + [currentWindow.HTMLInputElement.prototype, 'value'], + [currentWindow.HTMLInputElement.prototype, 'checked'], + [currentWindow.HTMLSelectElement.prototype, 'value'], + [currentWindow.HTMLTextAreaElement.prototype, 'value'], + [currentWindow.HTMLSelectElement.prototype, 'selectedIndex'], + [currentWindow.HTMLOptionElement.prototype, 'selected'], + ]; + if (propertyDescriptor && propertyDescriptor.set) { + handlers.push(...hookProperties.map((p) => hookSetter(p[0], p[1], { + set() { + callbackWrapper(eventHandler)({ + target: this, + isTrusted: false, + }); + }, + }, false, currentWindow))); + } + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function getNestedCSSRulePositions(rule) { + const positions = []; + function recurse(childRule, pos) { + if ((hasNestedCSSRule('CSSGroupingRule') && + childRule.parentRule instanceof CSSGroupingRule) || + (hasNestedCSSRule('CSSMediaRule') && + childRule.parentRule instanceof CSSMediaRule) || + (hasNestedCSSRule('CSSSupportsRule') && + childRule.parentRule instanceof CSSSupportsRule) || + (hasNestedCSSRule('CSSConditionRule') && + childRule.parentRule instanceof CSSConditionRule)) { + const rules = Array.from(childRule.parentRule.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + else if (childRule.parentStyleSheet) { + const rules = Array.from(childRule.parentStyleSheet.cssRules); + const index = rules.indexOf(childRule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); + } + function getIdAndStyleId(sheet, mirror, styleMirror) { + let id, styleId; + if (!sheet) + return {}; + if (sheet.ownerNode) + id = mirror.getId(sheet.ownerNode); + else + styleId = styleMirror.getId(sheet); + return { + styleId, + id, + }; + } + function initStyleSheetObserver({ styleSheetRuleCb, mirror, stylesheetManager }, { win }) { + if (!win.CSSStyleSheet || !win.CSSStyleSheet.prototype) { + return () => { + }; + } + const insertRule = win.CSSStyleSheet.prototype.insertRule; + win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [{ rule, index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const deleteRule = win.CSSStyleSheet.prototype.deleteRule; + win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [{ index }], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + let replace; + if (win.CSSStyleSheet.prototype.replace) { + replace = win.CSSStyleSheet.prototype.replace; + win.CSSStyleSheet.prototype.replace = new Proxy(replace, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replace: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + let replaceSync; + if (win.CSSStyleSheet.prototype.replaceSync) { + replaceSync = win.CSSStyleSheet.prototype.replaceSync; + win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [text] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + replaceSync: text, + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + } + const supportedNestedCSSRuleTypes = {}; + if (canMonkeyPatchNestedCSSRule('CSSGroupingRule')) { + supportedNestedCSSRuleTypes.CSSGroupingRule = win.CSSGroupingRule; + } + else { + if (canMonkeyPatchNestedCSSRule('CSSMediaRule')) { + supportedNestedCSSRuleTypes.CSSMediaRule = win.CSSMediaRule; + } + if (canMonkeyPatchNestedCSSRule('CSSConditionRule')) { + supportedNestedCSSRuleTypes.CSSConditionRule = win.CSSConditionRule; + } + if (canMonkeyPatchNestedCSSRule('CSSSupportsRule')) { + supportedNestedCSSRuleTypes.CSSSupportsRule = win.CSSSupportsRule; + } + } + const unmodifiedFunctions = {}; + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: type.prototype.insertRule, + deleteRule: type.prototype.deleteRule, + }; + type.prototype.insertRule = new Proxy(unmodifiedFunctions[typeKey].insertRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [rule, index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(thisArg), + index || 0, + ], + }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + type.prototype.deleteRule = new Proxy(unmodifiedFunctions[typeKey].deleteRule, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + const [index] = argumentsList; + const { id, styleId } = getIdAndStyleId(thisArg.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleSheetRuleCb({ + id, + styleId, + removes: [ + { index: [...getNestedCSSRulePositions(thisArg), index] }, + ], + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + }); + return callbackWrapper(() => { + win.CSSStyleSheet.prototype.insertRule = insertRule; + win.CSSStyleSheet.prototype.deleteRule = deleteRule; + replace && (win.CSSStyleSheet.prototype.replace = replace); + replaceSync && (win.CSSStyleSheet.prototype.replaceSync = replaceSync); + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; + type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; + }); + }); + } + function initAdoptedStyleSheetObserver({ mirror, stylesheetManager, }, host) { + var _a, _b, _c; + let hostId = null; + if (host.nodeName === '#document') + hostId = mirror.getId(host); + else + hostId = mirror.getId(host.host); + const patchTarget = host.nodeName === '#document' + ? (_a = host.defaultView) === null || _a === void 0 ? void 0 : _a.Document + : (_c = (_b = host.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView) === null || _c === void 0 ? void 0 : _c.ShadowRoot; + const originalPropertyDescriptor = (patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype) + ? Object.getOwnPropertyDescriptor(patchTarget === null || patchTarget === void 0 ? void 0 : patchTarget.prototype, 'adoptedStyleSheets') + : undefined; + if (hostId === null || + hostId === -1 || + !patchTarget || + !originalPropertyDescriptor) + return () => { + }; + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get() { + var _a; + return (_a = originalPropertyDescriptor.get) === null || _a === void 0 ? void 0 : _a.call(this); + }, + set(sheets) { + var _a; + const result = (_a = originalPropertyDescriptor.set) === null || _a === void 0 ? void 0 : _a.call(this, sheets); + if (hostId !== null && hostId !== -1) { + try { + stylesheetManager.adoptStyleSheets(sheets, hostId); + } + catch (e) { + } + } + return result; + }, + }); + return callbackWrapper(() => { + Object.defineProperty(host, 'adoptedStyleSheets', { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get: originalPropertyDescriptor.get, + set: originalPropertyDescriptor.set, + }); + }); + } + function initStyleDeclarationObserver({ styleDeclarationCb, mirror, ignoreCSSAttributes, stylesheetManager, }, { win }) { + const setProperty = win.CSSStyleDeclaration.prototype.setProperty; + win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property, value, priority] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return setProperty.apply(thisArg, [property, value, priority]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty; + win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, { + apply: callbackWrapper((target, thisArg, argumentsList) => { + var _a; + const [property] = argumentsList; + if (ignoreCSSAttributes.has(property)) { + return removeProperty.apply(thisArg, [property]); + } + const { id, styleId } = getIdAndStyleId((_a = thisArg.parentRule) === null || _a === void 0 ? void 0 : _a.parentStyleSheet, mirror, stylesheetManager.styleMirror); + if ((id && id !== -1) || (styleId && styleId !== -1)) { + styleDeclarationCb({ + id, + styleId, + remove: { + property, + }, + index: getNestedCSSRulePositions(thisArg.parentRule), + }); + } + return target.apply(thisArg, argumentsList); + }), + }); + return callbackWrapper(() => { + win.CSSStyleDeclaration.prototype.setProperty = setProperty; + win.CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }); + } + function initMediaInteractionObserver({ mediaInteractionCb, blockClass, blockSelector, mirror, sampling, doc, }) { + const handler = callbackWrapper((type) => throttle(callbackWrapper((event) => { + const target = getEventTarget(event); + if (!target || + isBlocked(target, blockClass, blockSelector, true)) { + return; + } + const { currentTime, volume, muted, playbackRate, loop } = target; + mediaInteractionCb({ + type, + id: mirror.getId(target), + currentTime, + volume, + muted, + playbackRate, + loop, + }); + }), sampling.media || 500)); + const handlers = [ + on('play', handler(0), doc), + on('pause', handler(1), doc), + on('seeked', handler(2), doc), + on('volumechange', handler(3), doc), + on('ratechange', handler(4), doc), + ]; + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initFontObserver({ fontCb, doc }) { + const win = doc.defaultView; + if (!win) { + return () => { + }; + } + const handlers = []; + const fontMap = new WeakMap(); + const originalFontFace = win.FontFace; + win.FontFace = function FontFace(family, source, descriptors) { + const fontFace = new originalFontFace(family, source, descriptors); + fontMap.set(fontFace, { + family, + buffer: typeof source !== 'string', + descriptors, + fontSource: typeof source === 'string' + ? source + : JSON.stringify(Array.from(new Uint8Array(source))), + }); + return fontFace; + }; + const restoreHandler = patch(doc.fonts, 'add', function (original) { + return function (fontFace) { + setTimeout(callbackWrapper(() => { + const p = fontMap.get(fontFace); + if (p) { + fontCb(p); + fontMap.delete(fontFace); + } + }), 0); + return original.apply(this, [fontFace]); + }; + }); + handlers.push(() => { + win.FontFace = originalFontFace; + }); + handlers.push(restoreHandler); + return callbackWrapper(() => { + handlers.forEach((h) => h()); + }); + } + function initSelectionObserver(param) { + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; + let collapsed = true; + const updateSelection = callbackWrapper(() => { + const selection = doc.getSelection(); + if (!selection || (collapsed && (selection === null || selection === void 0 ? void 0 : selection.isCollapsed))) + return; + collapsed = selection.isCollapsed || false; + const ranges = []; + const count = selection.rangeCount || 0; + for (let i = 0; i < count; i++) { + const range = selection.getRangeAt(i); + const { startContainer, startOffset, endContainer, endOffset } = range; + const blocked = isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); + if (blocked) + continue; + ranges.push({ + start: mirror.getId(startContainer), + startOffset, + end: mirror.getId(endContainer), + endOffset, + }); + } + selectionCb({ ranges }); + }); + updateSelection(); + return on('selectionchange', updateSelection); + } + function initCustomElementObserver({ doc, customElementCb, }) { + const win = doc.defaultView; + if (!win || !win.customElements) + return () => { }; + const restoreHandler = patch(win.customElements, 'define', function (original) { + return function (name, constructor, options) { + try { + customElementCb({ + define: { + name, + }, + }); + } + catch (e) { + console.warn(`Custom element callback failed for ${name}`); + } + return original.apply(this, [name, constructor, options]); + }; + }); + return restoreHandler; + } + function mergeHooks(o, hooks) { + const { mutationCb, mousemoveCb, mouseInteractionCb, scrollCb, viewportResizeCb, inputCb, mediaInteractionCb, styleSheetRuleCb, styleDeclarationCb, canvasMutationCb, fontCb, selectionCb, customElementCb, } = o; + o.mutationCb = (...p) => { + if (hooks.mutation) { + hooks.mutation(...p); + } + mutationCb(...p); + }; + o.mousemoveCb = (...p) => { + if (hooks.mousemove) { + hooks.mousemove(...p); + } + mousemoveCb(...p); + }; + o.mouseInteractionCb = (...p) => { + if (hooks.mouseInteraction) { + hooks.mouseInteraction(...p); + } + mouseInteractionCb(...p); + }; + o.scrollCb = (...p) => { + if (hooks.scroll) { + hooks.scroll(...p); + } + scrollCb(...p); + }; + o.viewportResizeCb = (...p) => { + if (hooks.viewportResize) { + hooks.viewportResize(...p); + } + viewportResizeCb(...p); + }; + o.inputCb = (...p) => { + if (hooks.input) { + hooks.input(...p); + } + inputCb(...p); + }; + o.mediaInteractionCb = (...p) => { + if (hooks.mediaInteaction) { + hooks.mediaInteaction(...p); + } + mediaInteractionCb(...p); + }; + o.styleSheetRuleCb = (...p) => { + if (hooks.styleSheetRule) { + hooks.styleSheetRule(...p); + } + styleSheetRuleCb(...p); + }; + o.styleDeclarationCb = (...p) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; + o.canvasMutationCb = (...p) => { + if (hooks.canvasMutation) { + hooks.canvasMutation(...p); + } + canvasMutationCb(...p); + }; + o.fontCb = (...p) => { + if (hooks.font) { + hooks.font(...p); + } + fontCb(...p); + }; + o.selectionCb = (...p) => { + if (hooks.selection) { + hooks.selection(...p); + } + selectionCb(...p); + }; + o.customElementCb = (...c) => { + if (hooks.customElement) { + hooks.customElement(...c); + } + customElementCb(...c); + }; + } + function initObservers(o, hooks = {}) { + const currentWindow = o.doc.defaultView; + if (!currentWindow) { + return () => { + }; + } + mergeHooks(o, hooks); + let mutationObserver; + if (o.recordDOM) { + mutationObserver = initMutationObserver(o, o.doc); + } + const mousemoveHandler = initMoveObserver(o); + const mouseInteractionHandler = initMouseInteractionObserver(o); + const scrollHandler = initScrollObserver(o); + const viewportResizeHandler = initViewportResizeObserver(o, { + win: currentWindow, + }); + const inputHandler = initInputObserver(o); + const mediaInteractionHandler = initMediaInteractionObserver(o); + let styleSheetObserver = () => { }; + let adoptedStyleSheetObserver = () => { }; + let styleDeclarationObserver = () => { }; + let fontObserver = () => { }; + if (o.recordDOM) { + styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow }); + adoptedStyleSheetObserver = initAdoptedStyleSheetObserver(o, o.doc); + styleDeclarationObserver = initStyleDeclarationObserver(o, { + win: currentWindow, + }); + if (o.collectFonts) { + fontObserver = initFontObserver(o); + } + } + const selectionObserver = initSelectionObserver(o); + const customElementObserver = initCustomElementObserver(o); + const pluginHandlers = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, currentWindow, plugin.options)); + } + return callbackWrapper(() => { + mutationBuffers.forEach((b) => b.reset()); + mutationObserver === null || mutationObserver === void 0 ? void 0 : mutationObserver.disconnect(); + mousemoveHandler(); + mouseInteractionHandler(); + scrollHandler(); + viewportResizeHandler(); + inputHandler(); + mediaInteractionHandler(); + styleSheetObserver(); + adoptedStyleSheetObserver(); + styleDeclarationObserver(); + fontObserver(); + selectionObserver(); + customElementObserver(); + pluginHandlers.forEach((h) => h()); + }); + } + function hasNestedCSSRule(prop) { + return typeof window[prop] !== 'undefined'; + } + function canMonkeyPatchNestedCSSRule(prop) { + return Boolean(typeof window[prop] !== 'undefined' && + window[prop].prototype && + 'insertRule' in window[prop].prototype && + 'deleteRule' in window[prop].prototype); + } + + class CrossOriginIframeMirror { + constructor(generateIdFn) { + this.generateIdFn = generateIdFn; + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + } + getId(iframe, remoteId, idToRemoteMap, remoteToIdMap) { + const idToRemoteIdMap = idToRemoteMap || this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = remoteToIdMap || this.getRemoteIdToIdMap(iframe); + let id = idToRemoteIdMap.get(remoteId); + if (!id) { + id = this.generateIdFn(); + idToRemoteIdMap.set(remoteId, id); + remoteIdToIdMap.set(id, remoteId); + } + return id; + } + getIds(iframe, remoteId) { + const idToRemoteIdMap = this.getIdToRemoteIdMap(iframe); + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return remoteId.map((id) => this.getId(iframe, id, idToRemoteIdMap, remoteIdToIdMap)); + } + getRemoteId(iframe, id, map) { + const remoteIdToIdMap = map || this.getRemoteIdToIdMap(iframe); + if (typeof id !== 'number') + return id; + const remoteId = remoteIdToIdMap.get(id); + if (!remoteId) + return -1; + return remoteId; + } + getRemoteIds(iframe, ids) { + const remoteIdToIdMap = this.getRemoteIdToIdMap(iframe); + return ids.map((id) => this.getRemoteId(iframe, id, remoteIdToIdMap)); + } + reset(iframe) { + if (!iframe) { + this.iframeIdToRemoteIdMap = new WeakMap(); + this.iframeRemoteIdToIdMap = new WeakMap(); + return; + } + this.iframeIdToRemoteIdMap.delete(iframe); + this.iframeRemoteIdToIdMap.delete(iframe); + } + getIdToRemoteIdMap(iframe) { + let idToRemoteIdMap = this.iframeIdToRemoteIdMap.get(iframe); + if (!idToRemoteIdMap) { + idToRemoteIdMap = new Map(); + this.iframeIdToRemoteIdMap.set(iframe, idToRemoteIdMap); + } + return idToRemoteIdMap; + } + getRemoteIdToIdMap(iframe) { + let remoteIdToIdMap = this.iframeRemoteIdToIdMap.get(iframe); + if (!remoteIdToIdMap) { + remoteIdToIdMap = new Map(); + this.iframeRemoteIdToIdMap.set(iframe, remoteIdToIdMap); + } + return remoteIdToIdMap; + } + } + + class IframeManager { + constructor(options) { + this.iframes = new WeakMap(); + this.crossOriginIframeMap = new WeakMap(); + this.crossOriginIframeMirror = new CrossOriginIframeMirror(genId); + this.crossOriginIframeRootIdMap = new WeakMap(); + this.mutationCb = options.mutationCb; + this.wrappedEmit = options.wrappedEmit; + this.stylesheetManager = options.stylesheetManager; + this.recordCrossOriginIframes = options.recordCrossOriginIframes; + this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(this.stylesheetManager.styleMirror.generateId.bind(this.stylesheetManager.styleMirror)); + this.mirror = options.mirror; + if (this.recordCrossOriginIframes) { + window.addEventListener('message', this.handleMessage.bind(this)); + } + } + addIframe(iframeEl) { + this.iframes.set(iframeEl, true); + if (iframeEl.contentWindow) + this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl); + } + addLoadListener(cb) { + this.loadListener = cb; + } + attachIframe(iframeEl, childSn) { + var _a; + this.mutationCb({ + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: childSn, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }); + (_a = this.loadListener) === null || _a === void 0 ? void 0 : _a.call(this, iframeEl); + if (iframeEl.contentDocument && + iframeEl.contentDocument.adoptedStyleSheets && + iframeEl.contentDocument.adoptedStyleSheets.length > 0) + this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument)); + } + handleMessage(message) { + const crossOriginMessageEvent = message; + if (crossOriginMessageEvent.data.type !== 'rrweb' || + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin) + return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) + return; + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) + return; + const transformedEvent = this.transformCrossOriginEvent(iframeEl, crossOriginMessageEvent.data.event); + if (transformedEvent) + this.wrappedEmit(transformedEvent, crossOriginMessageEvent.data.isCheckout); + } + transformCrossOriginEvent(iframeEl, e) { + var _a; + switch (e.type) { + case EventType$1.FullSnapshot: { + this.crossOriginIframeMirror.reset(iframeEl); + this.crossOriginIframeStyleMirror.reset(iframeEl); + this.replaceIdOnNode(e.data.node, iframeEl); + const rootId = e.data.node.id; + this.crossOriginIframeRootIdMap.set(iframeEl, rootId); + this.patchRootIdOnNode(e.data.node, rootId); + return { + timestamp: e.timestamp, + type: EventType$1.IncrementalSnapshot, + data: { + source: IncrementalSource$1.Mutation, + adds: [ + { + parentId: this.mirror.getId(iframeEl), + nextId: null, + node: e.data.node, + }, + ], + removes: [], + texts: [], + attributes: [], + isAttachIframe: true, + }, + }; + } + case EventType$1.Meta: + case EventType$1.Load: + case EventType$1.DomContentLoaded: { + return false; + } + case EventType$1.Plugin: { + return e; + } + case EventType$1.Custom: { + this.replaceIds(e.data.payload, iframeEl, ['id', 'parentId', 'previousId', 'nextId']); + return e; + } + case EventType$1.IncrementalSnapshot: { + switch (e.data.source) { + case IncrementalSource$1.Mutation: { + e.data.adds.forEach((n) => { + this.replaceIds(n, iframeEl, [ + 'parentId', + 'nextId', + 'previousId', + ]); + this.replaceIdOnNode(n.node, iframeEl); + const rootId = this.crossOriginIframeRootIdMap.get(iframeEl); + rootId && this.patchRootIdOnNode(n.node, rootId); + }); + e.data.removes.forEach((n) => { + this.replaceIds(n, iframeEl, ['parentId', 'id']); + }); + e.data.attributes.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + e.data.texts.forEach((n) => { + this.replaceIds(n, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.Drag: + case IncrementalSource$1.TouchMove: + case IncrementalSource$1.MouseMove: { + e.data.positions.forEach((p) => { + this.replaceIds(p, iframeEl, ['id']); + }); + return e; + } + case IncrementalSource$1.ViewportResize: { + return false; + } + case IncrementalSource$1.MediaInteraction: + case IncrementalSource$1.MouseInteraction: + case IncrementalSource$1.Scroll: + case IncrementalSource$1.CanvasMutation: + case IncrementalSource$1.Input: { + this.replaceIds(e.data, iframeEl, ['id']); + return e; + } + case IncrementalSource$1.StyleSheetRule: + case IncrementalSource$1.StyleDeclaration: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleId']); + return e; + } + case IncrementalSource$1.Font: { + return e; + } + case IncrementalSource$1.Selection: { + e.data.ranges.forEach((range) => { + this.replaceIds(range, iframeEl, ['start', 'end']); + }); + return e; + } + case IncrementalSource$1.AdoptedStyleSheet: { + this.replaceIds(e.data, iframeEl, ['id']); + this.replaceStyleIds(e.data, iframeEl, ['styleIds']); + (_a = e.data.styles) === null || _a === void 0 ? void 0 : _a.forEach((style) => { + this.replaceStyleIds(style, iframeEl, ['styleId']); + }); + return e; + } + } + } + } + return false; + } + replace(iframeMirror, obj, iframeEl, keys) { + for (const key of keys) { + if (!Array.isArray(obj[key]) && typeof obj[key] !== 'number') + continue; + if (Array.isArray(obj[key])) { + obj[key] = iframeMirror.getIds(iframeEl, obj[key]); + } + else { + obj[key] = iframeMirror.getId(iframeEl, obj[key]); + } + } + return obj; + } + replaceIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeMirror, obj, iframeEl, keys); + } + replaceStyleIds(obj, iframeEl, keys) { + return this.replace(this.crossOriginIframeStyleMirror, obj, iframeEl, keys); + } + replaceIdOnNode(node, iframeEl) { + this.replaceIds(node, iframeEl, ['id', 'rootId']); + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.replaceIdOnNode(child, iframeEl); + }); + } + } + patchRootIdOnNode(node, rootId) { + if (node.type !== NodeType.Document && !node.rootId) + node.rootId = rootId; + if ('childNodes' in node) { + node.childNodes.forEach((child) => { + this.patchRootIdOnNode(child, rootId); + }); + } + } + } + + class ShadowDomManager { + constructor(options) { + this.shadowDoms = new WeakSet(); + this.restoreHandlers = []; + this.mutationCb = options.mutationCb; + this.scrollCb = options.scrollCb; + this.bypassOptions = options.bypassOptions; + this.mirror = options.mirror; + this.init(); + } + init() { + this.reset(); + this.patchAttachShadow(Element, document); + } + addShadowRoot(shadowRoot, doc) { + if (!isNativeShadowDom(shadowRoot)) + return; + if (this.shadowDoms.has(shadowRoot)) + return; + this.shadowDoms.add(shadowRoot); + const observer = initMutationObserver(Object.assign(Object.assign({}, this.bypassOptions), { doc, mutationCb: this.mutationCb, mirror: this.mirror, shadowDomManager: this }), shadowRoot); + this.restoreHandlers.push(() => observer.disconnect()); + this.restoreHandlers.push(initScrollObserver(Object.assign(Object.assign({}, this.bypassOptions), { scrollCb: this.scrollCb, doc: shadowRoot, mirror: this.mirror }))); + setTimeout(() => { + if (shadowRoot.adoptedStyleSheets && + shadowRoot.adoptedStyleSheets.length > 0) + this.bypassOptions.stylesheetManager.adoptStyleSheets(shadowRoot.adoptedStyleSheets, this.mirror.getId(shadowRoot.host)); + this.restoreHandlers.push(initAdoptedStyleSheetObserver({ + mirror: this.mirror, + stylesheetManager: this.bypassOptions.stylesheetManager, + }, shadowRoot)); + }, 0); + } + observeAttachShadow(iframeElement) { + if (!iframeElement.contentWindow || !iframeElement.contentDocument) + return; + this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument); + } + patchAttachShadow(element, doc) { + const manager = this; + this.restoreHandlers.push(patch(element.prototype, 'attachShadow', function (original) { + return function (option) { + const shadowRoot = original.call(this, option); + if (this.shadowRoot && inDom(this)) + manager.addShadowRoot(this.shadowRoot, doc); + return shadowRoot; + }; + })); + } + reset() { + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } + catch (e) { + } + }); + this.restoreHandlers = []; + this.shadowDoms = new WeakSet(); + } + } + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; + } + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const canvasVarMap = new Map(); + function variableListFor(ctx, ctor) { + let contextMap = canvasVarMap.get(ctx); + if (!contextMap) { + contextMap = new Map(); + canvasVarMap.set(ctx, contextMap); + } + if (!contextMap.has(ctor)) { + contextMap.set(ctor, []); + } + return contextMap.get(ctor); + } + const saveWebGLVar = (value, win, ctx) => { + if (!value || + !(isInstanceOfWebGLObject(value, win) || typeof value === 'object')) + return; + const name = value.constructor.name; + const list = variableListFor(ctx, name); + let index = list.indexOf(value); + if (index === -1) { + index = list.length; + list.push(value); + } + return index; + }; + function serializeArg(value, win, ctx) { + if (value instanceof Array) { + return value.map((arg) => serializeArg(arg, win, ctx)); + } + else if (value === null) { + return value; + } + else if (value instanceof Float32Array || + value instanceof Float64Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Uint8Array || + value instanceof Uint16Array || + value instanceof Int16Array || + value instanceof Int8Array || + value instanceof Uint8ClampedArray) { + const name = value.constructor.name; + return { + rr_type: name, + args: [Object.values(value)], + }; + } + else if (value instanceof ArrayBuffer) { + const name = value.constructor.name; + const base64 = encode(value); + return { + rr_type: name, + base64, + }; + } + else if (value instanceof DataView) { + const name = value.constructor.name; + return { + rr_type: name, + args: [ + serializeArg(value.buffer, win, ctx), + value.byteOffset, + value.byteLength, + ], + }; + } + else if (value instanceof HTMLImageElement) { + const name = value.constructor.name; + const { src } = value; + return { + rr_type: name, + src, + }; + } + else if (value instanceof HTMLCanvasElement) { + const name = 'HTMLImageElement'; + const src = value.toDataURL(); + return { + rr_type: name, + src, + }; + } + else if (value instanceof ImageData) { + const name = value.constructor.name; + return { + rr_type: name, + args: [serializeArg(value.data, win, ctx), value.width, value.height], + }; + } + else if (isInstanceOfWebGLObject(value, win) || typeof value === 'object') { + const name = value.constructor.name; + const index = saveWebGLVar(value, win, ctx); + return { + rr_type: name, + index: index, + }; + } + return value; + } + const serializeArgs = (args, win, ctx) => { + return args.map((arg) => serializeArg(arg, win, ctx)); + }; + const isInstanceOfWebGLObject = (value, win) => { + const webGLConstructorNames = [ + 'WebGLActiveInfo', + 'WebGLBuffer', + 'WebGLFramebuffer', + 'WebGLProgram', + 'WebGLRenderbuffer', + 'WebGLShader', + 'WebGLShaderPrecisionFormat', + 'WebGLTexture', + 'WebGLUniformLocation', + 'WebGLVertexArrayObject', + 'WebGLVertexArrayObjectOES', + ]; + const supportedWebGLConstructorNames = webGLConstructorNames.filter((name) => typeof win[name] === 'function'); + return Boolean(supportedWebGLConstructorNames.find((name) => value instanceof win[name])); + }; + + function initCanvas2DMutationObserver(cb, win, blockClass, blockSelector) { + const handlers = []; + const props2D = Object.getOwnPropertyNames(win.CanvasRenderingContext2D.prototype); + for (const prop of props2D) { + try { + if (typeof win.CanvasRenderingContext2D.prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(win.CanvasRenderingContext2D.prototype, prop, function (original) { + return function (...args) { + if (!isBlocked(this.canvas, blockClass, blockSelector, true)) { + setTimeout(() => { + const recordArgs = serializeArgs(args, win, this); + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: recordArgs, + }); + }, 0); + } + return original.apply(this, args); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(win.CanvasRenderingContext2D.prototype, prop, { + set(v) { + cb(this.canvas, { + type: CanvasContext['2D'], + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function getNormalizedContextName(contextType) { + return contextType === 'experimental-webgl' ? 'webgl' : contextType; + } + function initCanvasContextObserver(win, blockClass, blockSelector, setPreserveDrawingBufferToTrue) { + const handlers = []; + try { + const restoreHandler = patch(win.HTMLCanvasElement.prototype, 'getContext', function (original) { + return function (contextType, ...args) { + if (!isBlocked(this, blockClass, blockSelector, true)) { + const ctxName = getNormalizedContextName(contextType); + if (!('__context' in this)) + this.__context = ctxName; + if (setPreserveDrawingBufferToTrue && + ['webgl', 'webgl2'].includes(ctxName)) { + if (args[0] && typeof args[0] === 'object') { + const contextAttributes = args[0]; + if (!contextAttributes.preserveDrawingBuffer) { + contextAttributes.preserveDrawingBuffer = true; + } + } + else { + args.splice(0, 1, { + preserveDrawingBuffer: true, + }); + } + } + } + return original.apply(this, [contextType, ...args]); + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + console.error('failed to patch HTMLCanvasElement.prototype.getContext'); + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function patchGLPrototype(prototype, type, cb, blockClass, blockSelector, mirror, win) { + const handlers = []; + const props = Object.getOwnPropertyNames(prototype); + for (const prop of props) { + if ([ + 'isContextLost', + 'canvas', + 'drawingBufferWidth', + 'drawingBufferHeight', + ].includes(prop)) { + continue; + } + try { + if (typeof prototype[prop] !== 'function') { + continue; + } + const restoreHandler = patch(prototype, prop, function (original) { + return function (...args) { + const result = original.apply(this, args); + saveWebGLVar(result, win, this); + if ('tagName' in this.canvas && + !isBlocked(this.canvas, blockClass, blockSelector, true)) { + const recordArgs = serializeArgs(args, win, this); + const mutation = { + type, + property: prop, + args: recordArgs, + }; + cb(this.canvas, mutation); + } + return result; + }; + }); + handlers.push(restoreHandler); + } + catch (_a) { + const hookHandler = hookSetter(prototype, prop, { + set(v) { + cb(this.canvas, { + type, + property: prop, + args: [v], + setter: true, + }); + }, + }); + handlers.push(hookHandler); + } + } + return handlers; + } + function initCanvasWebGLMutationObserver(cb, win, blockClass, blockSelector, mirror) { + const handlers = []; + handlers.push(...patchGLPrototype(win.WebGLRenderingContext.prototype, CanvasContext.WebGL, cb, blockClass, blockSelector, mirror, win)); + if (typeof win.WebGL2RenderingContext !== 'undefined') { + handlers.push(...patchGLPrototype(win.WebGL2RenderingContext.prototype, CanvasContext.WebGL2, cb, blockClass, blockSelector, mirror, win)); + } + return () => { + handlers.forEach((h) => h()); + }; + } + + function funcToSource(fn, sourcemapArg) { + var sourcemap = sourcemapArg === undefined ? null : sourcemapArg; + var source = fn.toString(); + var lines = source.split('\n'); + lines.pop(); + lines.shift(); + var blankPrefixLength = lines[0].search(/\S/); + var regex = /(['"])__worker_loader_strict__(['"])/g; + for (var i = 0, n = lines.length; i < n; ++i) { + lines[i] = lines[i].substring(blankPrefixLength).replace(regex, '$1use strict$2') + '\n'; + } + if (sourcemap) { + lines.push('\/\/# sourceMappingURL=' + sourcemap + '\n'); + } + return lines; + } + + function createURL(fn, sourcemapArg) { + var lines = funcToSource(fn, sourcemapArg); + var blob = new Blob(lines, { type: 'application/javascript' }); + return URL.createObjectURL(blob); + } + + function createInlineWorkerFactory(fn, sourcemapArg) { + var url; + return function WorkerFactory(options) { + url = url || createURL(fn, sourcemapArg); + return new Worker(url, options); + }; + } + + var WorkerFactory = createInlineWorkerFactory(/* rollup-plugin-web-worker-loader */function () { + (function () { + '__worker_loader_strict__'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + /* + * base64-arraybuffer 1.0.1 + * Copyright (c) 2021 Niklas von Hertzen + * Released under MIT License + */ + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + // Use a lookup table to find the index. + var lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); + for (var i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + var encode = function (arraybuffer) { + var bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = ''; + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } + else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + return base64; + }; + + const lastBlobMap = new Map(); + const transparentBlobMap = new Map(); + function getTransparentBlobFor(width, height, dataURLOptions) { + return __awaiter(this, void 0, void 0, function* () { + const id = `${width}-${height}`; + if ('OffscreenCanvas' in globalThis) { + if (transparentBlobMap.has(id)) + return transparentBlobMap.get(id); + const offscreen = new OffscreenCanvas(width, height); + offscreen.getContext('2d'); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + transparentBlobMap.set(id, base64); + return base64; + } + else { + return ''; + } + }); + } + const worker = self; + worker.onmessage = function (e) { + return __awaiter(this, void 0, void 0, function* () { + if ('OffscreenCanvas' in globalThis) { + const { id, bitmap, width, height, dataURLOptions } = e.data; + const transparentBase64 = getTransparentBlobFor(width, height, dataURLOptions); + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext('2d'); + ctx.drawImage(bitmap, 0, 0); + bitmap.close(); + const blob = yield offscreen.convertToBlob(dataURLOptions); + const type = blob.type; + const arrayBuffer = yield blob.arrayBuffer(); + const base64 = encode(arrayBuffer); + if (!lastBlobMap.has(id) && (yield transparentBase64) === base64) { + lastBlobMap.set(id, base64); + return worker.postMessage({ id }); + } + if (lastBlobMap.get(id) === base64) + return worker.postMessage({ id }); + worker.postMessage({ + id, + type, + base64, + width, + height, + }); + lastBlobMap.set(id, base64); + } + else { + return worker.postMessage({ id: e.data.id }); + } + }); + }; + + })(); + }, null); + + class CanvasManager { + reset() { + this.pendingCanvasMutations.clear(); + this.resetObservers && this.resetObservers(); + } + freeze() { + this.frozen = true; + } + unfreeze() { + this.frozen = false; + } + lock() { + this.locked = true; + } + unlock() { + this.locked = false; + } + constructor(options) { + this.pendingCanvasMutations = new Map(); + this.rafStamps = { latestId: 0, invokeId: null }; + this.frozen = false; + this.locked = false; + this.processMutation = (target, mutation) => { + const newFrame = this.rafStamps.invokeId && + this.rafStamps.latestId !== this.rafStamps.invokeId; + if (newFrame || !this.rafStamps.invokeId) + this.rafStamps.invokeId = this.rafStamps.latestId; + if (!this.pendingCanvasMutations.has(target)) { + this.pendingCanvasMutations.set(target, []); + } + this.pendingCanvasMutations.get(target).push(mutation); + }; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas, dataURLOptions, } = options; + this.mutationCb = options.mutationCb; + this.mirror = options.mirror; + if (recordCanvas && sampling === 'all') + this.initCanvasMutationObserver(win, blockClass, blockSelector); + if (recordCanvas && typeof sampling === 'number') + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector, { + dataURLOptions, + }); + } + initCanvasFPSObserver(fps, win, blockClass, blockSelector, options) { + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, true); + const snapshotInProgressMap = new Map(); + const worker = new WorkerFactory(); + worker.onmessage = (e) => { + const { id } = e.data; + snapshotInProgressMap.set(id, false); + if (!('base64' in e.data)) + return; + const { base64, type, width, height } = e.data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', + args: [0, 0, width, height], + }, + { + property: 'drawImage', + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + }, + 0, + 0, + ], + }, + ], + }); + }; + const timeBetweenSnapshots = 1000 / fps; + let lastSnapshotTime = 0; + let rafId; + const getCanvas = () => { + const matchedCanvas = []; + win.document.querySelectorAll('canvas').forEach((canvas) => { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { + matchedCanvas.push(canvas); + } + }); + return matchedCanvas; + }; + const takeCanvasSnapshots = (timestamp) => { + if (lastSnapshotTime && + timestamp - lastSnapshotTime < timeBetweenSnapshots) { + rafId = requestAnimationFrame(takeCanvasSnapshots); + return; + } + lastSnapshotTime = timestamp; + getCanvas() + .forEach((canvas) => __awaiter(this, void 0, void 0, function* () { + var _a; + const id = this.mirror.getId(canvas); + if (snapshotInProgressMap.get(id)) + return; + if (canvas.width === 0 || canvas.height === 0) + return; + snapshotInProgressMap.set(id, true); + if (['webgl', 'webgl2'].includes(canvas.__context)) { + const context = canvas.getContext(canvas.__context); + if (((_a = context === null || context === void 0 ? void 0 : context.getContextAttributes()) === null || _a === void 0 ? void 0 : _a.preserveDrawingBuffer) === false) { + context.clear(context.COLOR_BUFFER_BIT); + } + } + const bitmap = yield createImageBitmap(canvas); + worker.postMessage({ + id, + bitmap, + width: canvas.width, + height: canvas.height, + dataURLOptions: options.dataURLOptions, + }, [bitmap]); + })); + rafId = requestAnimationFrame(takeCanvasSnapshots); + }; + rafId = requestAnimationFrame(takeCanvasSnapshots); + this.resetObservers = () => { + canvasContextReset(); + cancelAnimationFrame(rafId); + }; + } + initCanvasMutationObserver(win, blockClass, blockSelector) { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector, false); + const canvas2DReset = initCanvas2DMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector); + const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(this.processMutation.bind(this), win, blockClass, blockSelector, this.mirror); + this.resetObservers = () => { + canvasContextReset(); + canvas2DReset(); + canvasWebGL1and2Reset(); + }; + } + startPendingCanvasMutationFlusher() { + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + startRAFTimestamping() { + const setLatestRAFTimestamp = (timestamp) => { + this.rafStamps.latestId = timestamp; + requestAnimationFrame(setLatestRAFTimestamp); + }; + requestAnimationFrame(setLatestRAFTimestamp); + } + flushPendingCanvasMutations() { + this.pendingCanvasMutations.forEach((values, canvas) => { + const id = this.mirror.getId(canvas); + this.flushPendingCanvasMutationFor(canvas, id); + }); + requestAnimationFrame(() => this.flushPendingCanvasMutations()); + } + flushPendingCanvasMutationFor(canvas, id) { + if (this.frozen || this.locked) { + return; + } + const valuesWithType = this.pendingCanvasMutations.get(canvas); + if (!valuesWithType || id === -1) + return; + const values = valuesWithType.map((value) => { + const rest = __rest(value, ["type"]); + return rest; + }); + const { type } = valuesWithType[0]; + this.mutationCb({ id, type, commands: values }); + this.pendingCanvasMutations.delete(canvas); + } + } + + class StylesheetManager { + constructor(options) { + this.trackedLinkElements = new WeakSet(); + this.styleMirror = new StyleSheetMirror(); + this.mutationCb = options.mutationCb; + this.adoptedStyleSheetCb = options.adoptedStyleSheetCb; + } + attachLinkElement(linkEl, childSn) { + if ('_cssText' in childSn.attributes) + this.mutationCb({ + adds: [], + removes: [], + texts: [], + attributes: [ + { + id: childSn.id, + attributes: childSn + .attributes, + }, + ], + }); + this.trackLinkElement(linkEl); + } + trackLinkElement(linkEl) { + if (this.trackedLinkElements.has(linkEl)) + return; + this.trackedLinkElements.add(linkEl); + this.trackStylesheetInLinkElement(linkEl); + } + adoptStyleSheets(sheets, hostId) { + if (sheets.length === 0) + return; + const adoptedStyleSheetData = { + id: hostId, + styleIds: [], + }; + const styles = []; + for (const sheet of sheets) { + let styleId; + if (!this.styleMirror.has(sheet)) { + styleId = this.styleMirror.add(sheet); + styles.push({ + styleId, + rules: Array.from(sheet.rules || CSSRule, (r, index) => ({ + rule: stringifyRule(r), + index, + })), + }); + } + else + styleId = this.styleMirror.getId(sheet); + adoptedStyleSheetData.styleIds.push(styleId); + } + if (styles.length > 0) + adoptedStyleSheetData.styles = styles; + this.adoptedStyleSheetCb(adoptedStyleSheetData); + } + reset() { + this.styleMirror.reset(); + this.trackedLinkElements = new WeakSet(); + } + trackStylesheetInLinkElement(linkEl) { + } + } + + class ProcessedNodeManager { + constructor() { + this.nodeMap = new WeakMap(); + this.loop = true; + this.periodicallyClear(); + } + periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + if (this.loop) + this.periodicallyClear(); + }); + } + inOtherBuffer(node, thisBuffer) { + const buffers = this.nodeMap.get(node); + return (buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer)); + } + add(node, buffer) { + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); + } + clear() { + this.nodeMap = new WeakMap(); + } + destroy() { + this.loop = false; + } + } + + function wrapEvent(e) { + return Object.assign(Object.assign({}, e), { timestamp: nowTimestamp() }); + } + let wrappedEmit; + let takeFullSnapshot; + let canvasManager; + let recording = false; + const mirror = createMirror(); + function record(options = {}) { + const { emit, checkoutEveryNms, checkoutEveryNth, blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', ignoreSelector = null, maskTextClass = 'rr-mask', maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, maskTextFn, hooks, packFn, sampling = {}, dataURLOptions = {}, mousemoveWait, recordDOM = true, recordCanvas = false, recordCrossOriginIframes = false, recordAfter = options.recordAfter === 'DOMContentLoaded' + ? options.recordAfter + : 'load', userTriggeredOnInput = false, collectFonts = false, inlineImages = false, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), errorHandler, } = options; + registerErrorHandler(errorHandler); + const inEmittingFrame = recordCrossOriginIframes + ? window.parent === window + : true; + let passEmitsToParent = false; + if (!inEmittingFrame) { + try { + if (window.parent.document) { + passEmitsToParent = false; + } + } + catch (e) { + passEmitsToParent = true; + } + } + if (inEmittingFrame && !emit) { + throw new Error('emit function is required'); + } + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + mirror.reset(); + const maskInputOptions = maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + password: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : { password: true }; + const slimDOMOptions = _slimDOMOptions === true || _slimDOMOptions === 'all' + ? { + script: true, + comment: true, + headFavicon: true, + headWhitespace: true, + headMetaSocial: true, + headMetaRobots: true, + headMetaHttpEquiv: true, + headMetaVerification: true, + headMetaAuthorship: _slimDOMOptions === 'all', + headMetaDescKeywords: _slimDOMOptions === 'all', + } + : _slimDOMOptions + ? _slimDOMOptions + : {}; + polyfill(); + let lastFullSnapshotEvent; + let incrementalSnapshotCount = 0; + const eventProcessor = (e) => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn && + !passEmitsToParent) { + e = packFn(e); + } + return e; + }; + wrappedEmit = (e, isCheckout) => { + var _a; + if (((_a = mutationBuffers[0]) === null || _a === void 0 ? void 0 : _a.isFrozen()) && + e.type !== EventType$1.FullSnapshot && + !(e.type === EventType$1.IncrementalSnapshot && + e.data.source === IncrementalSource$1.Mutation)) { + mutationBuffers.forEach((buf) => buf.unfreeze()); + } + if (inEmittingFrame) { + emit === null || emit === void 0 ? void 0 : emit(eventProcessor(e), isCheckout); + } + else if (passEmitsToParent) { + const message = { + type: 'rrweb', + event: eventProcessor(e), + origin: window.location.origin, + isCheckout, + }; + window.parent.postMessage(message, '*'); + } + if (e.type === EventType$1.FullSnapshot) { + lastFullSnapshotEvent = e; + incrementalSnapshotCount = 0; + } + else if (e.type === EventType$1.IncrementalSnapshot) { + if (e.data.source === IncrementalSource$1.Mutation && + e.data.isAttachIframe) { + return; + } + incrementalSnapshotCount++; + const exceedCount = checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth; + const exceedTime = checkoutEveryNms && + e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms; + if (exceedCount || exceedTime) { + takeFullSnapshot(true); + } + } + }; + const wrappedMutationEmit = (m) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Mutation }, m), + })); + }; + const wrappedScrollEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Scroll }, p), + })); + const wrappedCanvasMutationEmit = (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CanvasMutation }, p), + })); + const wrappedAdoptedStyleSheetEmit = (a) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.AdoptedStyleSheet }, a), + })); + const stylesheetManager = new StylesheetManager({ + mutationCb: wrappedMutationEmit, + adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit, + }); + const iframeManager = new IframeManager({ + mirror, + mutationCb: wrappedMutationEmit, + stylesheetManager: stylesheetManager, + recordCrossOriginIframes, + wrappedEmit, + }); + for (const plugin of plugins || []) { + if (plugin.getMirror) + plugin.getMirror({ + nodeMirror: mirror, + crossOriginIframeMirror: iframeManager.crossOriginIframeMirror, + crossOriginIframeStyleMirror: iframeManager.crossOriginIframeStyleMirror, + }); + } + const processedNodeManager = new ProcessedNodeManager(); + canvasManager = new CanvasManager({ + recordCanvas, + mutationCb: wrappedCanvasMutationEmit, + win: window, + blockClass, + blockSelector, + mirror, + sampling: sampling.canvas, + dataURLOptions, + }); + const shadowDomManager = new ShadowDomManager({ + mutationCb: wrappedMutationEmit, + scrollCb: wrappedScrollEmit, + bypassOptions: { + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskInputOptions, + dataURLOptions, + maskTextFn, + maskInputFn, + recordCanvas, + inlineImages, + sampling, + slimDOMOptions, + iframeManager, + stylesheetManager, + canvasManager, + keepIframeSrcFn, + processedNodeManager, + }, + mirror, + }); + takeFullSnapshot = (isCheckout = false) => { + if (!recordDOM) { + return; + } + wrappedEmit(wrapEvent({ + type: EventType$1.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), isCheckout); + stylesheetManager.reset(); + shadowDomManager.init(); + mutationBuffers.forEach((buf) => buf.lock()); + const node = snapshot(document, { + mirror, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + inlineStylesheet, + maskAllInputs: maskInputOptions, + maskTextFn, + slimDOM: slimDOMOptions, + dataURLOptions, + recordCanvas, + inlineImages, + onSerialize: (n) => { + if (isSerializedIframe(n, mirror)) { + iframeManager.addIframe(n); + } + if (isSerializedStylesheet(n, mirror)) { + stylesheetManager.trackLinkElement(n); + } + if (hasShadowRoot(n)) { + shadowDomManager.addShadowRoot(n.shadowRoot, document); + } + }, + onIframeLoad: (iframe, childSn) => { + iframeManager.attachIframe(iframe, childSn); + shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (linkEl, childSn) => { + stylesheetManager.attachLinkElement(linkEl, childSn); + }, + keepIframeSrcFn, + }); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.FullSnapshot, + data: { + node, + initialOffset: getWindowScroll(window), + }, + }), isCheckout); + mutationBuffers.forEach((buf) => buf.unlock()); + if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + stylesheetManager.adoptStyleSheets(document.adoptedStyleSheets, mirror.getId(document)); + }; + try { + const handlers = []; + const observe = (doc) => { + var _a; + return callbackWrapper(initObservers)({ + mutationCb: wrappedMutationEmit, + mousemoveCb: (positions, source) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: { + source, + positions, + }, + })), + mouseInteractionCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MouseInteraction }, d), + })), + scrollCb: wrappedScrollEmit, + viewportResizeCb: (d) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.ViewportResize }, d), + })), + inputCb: (v) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Input }, v), + })), + mediaInteractionCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.MediaInteraction }, p), + })), + styleSheetRuleCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleSheetRule }, r), + })), + styleDeclarationCb: (r) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.StyleDeclaration }, r), + })), + canvasMutationCb: wrappedCanvasMutationEmit, + fontCb: (p) => wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Font }, p), + })), + selectionCb: (p) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.Selection }, p), + })); + }, + customElementCb: (c) => { + wrappedEmit(wrapEvent({ + type: EventType$1.IncrementalSnapshot, + data: Object.assign({ source: IncrementalSource$1.CustomElement }, c), + })); + }, + blockClass, + ignoreClass, + ignoreSelector, + maskTextClass, + maskTextSelector, + maskInputOptions, + inlineStylesheet, + sampling, + recordDOM, + recordCanvas, + inlineImages, + userTriggeredOnInput, + collectFonts, + doc, + maskInputFn, + maskTextFn, + keepIframeSrcFn, + blockSelector, + slimDOMOptions, + dataURLOptions, + mirror, + iframeManager, + stylesheetManager, + shadowDomManager, + processedNodeManager, + canvasManager, + ignoreCSSAttributes, + plugins: ((_a = plugins === null || plugins === void 0 ? void 0 : plugins.filter((p) => p.observer)) === null || _a === void 0 ? void 0 : _a.map((p) => ({ + observer: p.observer, + options: p.options, + callback: (payload) => wrappedEmit(wrapEvent({ + type: EventType$1.Plugin, + data: { + plugin: p.name, + payload, + }, + })), + }))) || [], + }, hooks); + }; + iframeManager.addLoadListener((iframeEl) => { + try { + handlers.push(observe(iframeEl.contentDocument)); + } + catch (error) { + console.warn(error); + } + }); + const init = () => { + takeFullSnapshot(); + handlers.push(observe(document)); + recording = true; + }; + if (document.readyState === 'interactive' || + document.readyState === 'complete') { + init(); + } + else { + handlers.push(on('DOMContentLoaded', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.DomContentLoaded, + data: {}, + })); + if (recordAfter === 'DOMContentLoaded') + init(); + })); + handlers.push(on('load', () => { + wrappedEmit(wrapEvent({ + type: EventType$1.Load, + data: {}, + })); + if (recordAfter === 'load') + init(); + }, window)); + } + return () => { + handlers.forEach((h) => h()); + processedNodeManager.destroy(); + recording = false; + unregisterErrorHandler(); + }; + } + catch (error) { + console.warn(error); + } + } + record.addCustomEvent = (tag, payload) => { + if (!recording) { + throw new Error('please add custom event after start recording'); + } + wrappedEmit(wrapEvent({ + type: EventType$1.Custom, + data: { + tag, + payload, + }, + })); + }; + record.freezePage = () => { + mutationBuffers.forEach((buf) => buf.freeze()); + }; + record.takeFullSnapshot = (isCheckout) => { + if (!recording) { + throw new Error('please take full snapshot after start recording'); + } + takeFullSnapshot(isCheckout); + }; + record.mirror = mirror; + + var EventType = /* @__PURE__ */ ((EventType2) => { + EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded"; + EventType2[EventType2["Load"] = 1] = "Load"; + EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot"; + EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot"; + EventType2[EventType2["Meta"] = 4] = "Meta"; + EventType2[EventType2["Custom"] = 5] = "Custom"; + EventType2[EventType2["Plugin"] = 6] = "Plugin"; + return EventType2; + })(EventType || {}); + var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => { + IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation"; + IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove"; + IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction"; + IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll"; + IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize"; + IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input"; + IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove"; + IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction"; + IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule"; + IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation"; + IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font"; + IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log"; + IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag"; + IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration"; + IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection"; + IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet"; + IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement"; + return IncrementalSource2; + })(IncrementalSource || {}); + var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; /* eslint camelcase: "off", eqeqeq: "off" */ @@ -129,7 +4636,7 @@ }; // Console override - var console = { + var console$1 = { /** @type {function(...*)} */ log: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { @@ -186,14 +4693,14 @@ var log_func_with_prefix = function(func, prefix) { return function() { arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); + return func.apply(console$1, arguments); }; }; var console_with_prefix = function(prefix) { return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) + log: log_func_with_prefix(console$1.log, prefix), + error: log_func_with_prefix(console$1.error, prefix), + critical: log_func_with_prefix(console$1.critical, prefix) }; }; @@ -1046,7 +5553,7 @@ try { result = decodeURIComponent(result); } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); + console$1.error('Skipping decoding for malformed query param: ' + result); } return result.replace(/\+/g, ' '); } @@ -1173,13 +5680,13 @@ is_supported: function(force_check) { var supported = localStorageSupported(null, force_check); if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); + console$1.error('localStorage unsupported; falling back to cookie store'); } return supported; }, error: function(msg) { - console.error('localStorage error: ' + msg); + console$1.error('localStorage error: ' + msg); }, get: function(name) { @@ -1234,7 +5741,7 @@ */ var register_event = function(element, type, handler, oldSchool, useCapture) { if (!element) { - console.error('No valid element provided to register_event'); + console$1.error('No valid element provided to register_event'); return; } @@ -1796,158 +6303,306 @@ _['info']['browserVersion'] = _.info.browserVersion; _['info']['properties'] = _.info.properties; - /* eslint camelcase: "off" */ - /** - * DomTracker Object - * @constructor + * GDPR utils + * + * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection + * and privacy for all individuals within the European Union. It addresses the export of personal + * data outside the EU. The GDPR aims primarily to give control back to citizens and residents + * over their personal data and to simplify the regulatory environment for international business + * by unifying the regulation within the EU. + * + * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. + * These functions are used internally by the SDK and are not intended to be publicly exposed. */ - var DomTracker = function() {}; - - - // interface - DomTracker.prototype.create_properties = function() {}; - DomTracker.prototype.event_handler = function() {}; - DomTracker.prototype.after_track_handler = function() {}; - - DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; - }; /** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback + * A function used to track a Mixpanel event (e.g. MixpanelLib.track) + * @callback trackFunction + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ - DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - that.event_handler(e, this, options); + /** Public **/ - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); + /** + * Opt the user in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function optIn(token, options) { + _optInOut(true, token, options); + } - return true; - }; + /** + * Opt the user out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not + */ + function optOut(token, options) { + _optInOut(false, token, options); + } /** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured + * Check whether the user has opted in to data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {boolean} whether the user has opted in to the given opt type */ - DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; + function hasOptedIn(token, options) { + return _getStorageValue(token, options) === '1'; + } - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; + /** + * Check whether the user has opted out of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the user has opted out of the given opt type + */ + function hasOptedOut(token, options) { + if (_hasDoNotTrackFlagOn(options)) { + console$1.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); + return true; + } + var optedOut = _getStorageValue(token, options) === '0'; + if (optedOut) { + console$1.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + } + return optedOut; + } - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } + /** + * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelLib(method) { + return _addOptOutCheck(method, function(name) { + return this.get_config(name); + }); + } - that.after_track_handler(props, options, timeout_occured); - }; - }; + /** + * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelPeople(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); + } - DomTracker.prototype.create_properties = function(properties, element) { - var props; + /** + * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @returns {*} the result of executing method OR undefined if the user has opted out + */ + function addOptOutCheckMixpanelGroup(method) { + return _addOptOutCheck(method, function(name) { + return this._get_config(name); + }); + } - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } + /** + * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function clearOptInOut(token, options) { + options = options || {}; + _getStorage(options).remove( + _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain + ); + } - return props; - }; + /** Private **/ /** - * LinkTracker Object - * @constructor - * @extends DomTracker + * Get storage util + * @param {Object} [options] + * @param {string} [options.persistenceType] + * @returns {object} either _.cookie or _.localstorage */ - var LinkTracker = function() { - this.override_event = 'click'; - }; - _.inherit(LinkTracker, DomTracker); + function _getStorage(options) { + options = options || {}; + return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; + } - LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); + /** + * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the name of the cookie for the given opt type + */ + function _getStorageKey(token, options) { + options = options || {}; + return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; + } - if (element.href) { props['url'] = element.href; } + /** + * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @returns {string} the value of the cookie for the given opt type + */ + function _getStorageValue(token, options) { + return _getStorage(options).get(_getStorageKey(token, options)); + } - return props; - }; + /** + * Check whether the user has set the DNT/doNotTrack setting to true in their browser + * @param {Object} [options] + * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests + * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false + * @returns {boolean} whether the DNT setting is true + */ + function _hasDoNotTrackFlagOn(options) { + if (options && options.ignoreDnt) { + return false; + } + var win$1 = (options && options.window) || win; + var nav = win$1['navigator'] || {}; + var hasDntOn = false; - LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; + _.each([ + nav['doNotTrack'], // standard + nav['msDoNotTrack'], + win$1['doNotTrack'] + ], function(dntValue) { + if (_.includes([true, 1, '1', 'yes'], dntValue)) { + hasDntOn = true; + } + }); - if (!options.new_tab) { - evt.preventDefault(); + return hasDntOn; + } + + /** + * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type + * @param {boolean} optValue - whether to opt the user in or out for the given opt type + * @param {string} token - Mixpanel project tracking token + * @param {Object} [options] + * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action + * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action + * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action + * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name + * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires + * @param {string} [options.cookieDomain] - custom cookie domain + * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled + * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not + * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + */ + function _optInOut(optValue, token, options) { + if (!_.isString(token) || !token.length) { + console$1.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + return; } - }; - LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } + options = options || {}; - setTimeout(function() { - window.location = options.href; - }, 0); - }; + _getStorage(options).set( + _getStorageKey(token, options), + optValue ? 1 : 0, + _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, + !!options.crossSubdomainCookie, + !!options.secureCookie, + !!options.crossSiteCookie, + options.cookieDomain + ); + + if (options.track && optValue) { // only track event if opting in (optValue=true) + options.track(options.trackEventName || '$opt_in', options.trackProperties, { + 'send_immediately': true + }); + } + } /** - * FormTracker Object - * @constructor - * @extends DomTracker + * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token + * If the user has opted out, return early instead of executing the method. + * If a callback argument was provided, execute it passing the 0 error code. + * @param {function} method - wrapped method to be executed if the user has not opted out + * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check + * @returns {*} the result of executing method OR undefined if the user has opted out */ - var FormTracker = function() { - this.override_event = 'submit'; - }; - _.inherit(FormTracker, DomTracker); + function _addOptOutCheck(method, getConfigValue) { + return function() { + var optedOut = false; - FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); - }; + try { + var token = getConfigValue.call(this, 'token'); + var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); + var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); + var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); + var win = getConfigValue.call(this, 'window'); // used to override window during browser tests - FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); - }; + if (token) { // if there was an issue getting the token, continue method execution as normal + optedOut = hasOptedOut(token, { + ignoreDnt: ignoreDnt, + persistenceType: persistenceType, + persistencePrefix: persistencePrefix, + window: win + }); + } + } catch(err) { + console$1.error('Unexpected error when checking tracking opt-out status: ' + err); + } + + if (!optedOut) { + return method.apply(this, arguments); + } - var logger$2 = console_with_prefix('lock'); + var callback = arguments[arguments.length - 1]; + if (typeof(callback) === 'function') { + callback(0); + } + + return; + }; + } + + var logger$3 = console_with_prefix('lock'); /** * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser @@ -2004,7 +6659,7 @@ var delay = function(cb) { if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); + logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); storage.removeItem(keyZ); storage.removeItem(keyY); loop(); @@ -2093,7 +6748,7 @@ } }; - var logger$1 = console_with_prefix('batch'); + var logger$2 = console_with_prefix('batch'); /** * RequestQueue: queue for batching API requests with localStorage backup for retries. @@ -2115,9 +6770,10 @@ options = options || {}; this.storageKey = storageKey; this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); + this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2); this.lock = new SharedLock(storageKey, {storage: this.storage}); + this.usePersistence = options.usePersistence; this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios this.memQueue = []; @@ -2142,29 +6798,36 @@ 'payload': item }; - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } + if (!this.usePersistence) { + this.memQueue.push(queueEntry); if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue.push(queueEntry); + succeeded = this.saveToStorage(storedQueue); + if (succeeded) { + // only add to in-memory queue when storage succeeds + this.memQueue.push(queueEntry); + } + } catch(err) { + this.reportError('Error enqueueing item', item); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } }; /** @@ -2175,7 +6838,7 @@ */ RequestQueue.prototype.fillBatch = function(batchSize) { var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { + if (this.usePersistence && batch.length < batchSize) { // don't need lock just to read events; localStorage is thread-safe // and the worst that could happen is a duplicate send of some // orphaned events, which will be deduplicated on the server side @@ -2224,61 +6887,67 @@ _.each(ids, function(id) { idSet[id] = true; }); this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; + if (!this.usePersistence) { + if (cb) { + cb(true); + } + } else { + var removeFromStorage = _.bind(function() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); + succeeded = this.saveToStorage(storedQueue); + + // an extra check: did storage report success but somehow + // the items are still there? + if (succeeded) { + storedQueue = this.readFromStorage(); + for (var i = 0; i < storedQueue.length; i++) { + var item = storedQueue[i]; + if (item['id'] && !!idSet[item['id']]) { + this.reportError('Item not removed from storage'); + return false; + } } } + } catch(err) { + this.reportError('Error removing items', ids); + succeeded = false; } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); + return succeeded; + }, this); - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); + this.lock.withLock(function lockAcquired() { + var succeeded = removeFromStorage(); + if (cb) { + cb(succeeded); + } + }, _.bind(function lockFailure(err) { + var succeeded = false; + this.reportError('Error acquiring storage lock', err); + if (!localStorageSupported(this.storage, true)) { + // Looks like localStorage writes have stopped working sometime after + // initialization (probably full), and so nobody can acquire locks + // anymore. Consider it temporarily safe to remove items without the + // lock, since nobody's writing successfully anyway. + succeeded = removeFromStorage(); + if (!succeeded) { + // OK, we couldn't even write out the smaller queue. Try clearing it + // entirely. + try { + this.storage.removeItem(this.storageKey); + } catch(err) { + this.reportError('Error clearing queue', err); + } } } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); + if (cb) { + cb(succeeded); + } + }, this), this.pid); + } + }; // internal helper for RequestQueue.updatePayloads @@ -2306,25 +6975,32 @@ */ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } + if (!this.usePersistence) { if (cb) { - cb(succeeded); + cb(true); } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); + } else { + this.lock.withLock(_.bind(function lockAcquired() { + var succeeded; + try { + var storedQueue = this.readFromStorage(); + storedQueue = updatePayloads(storedQueue, itemsToUpdate); + succeeded = this.saveToStorage(storedQueue); + } catch(err) { + this.reportError('Error updating items', itemsToUpdate); + succeeded = false; + } + if (cb) { + cb(succeeded); + } + }, this), _.bind(function lockFailure(err) { + this.reportError('Error acquiring storage lock', err); + if (cb) { + cb(false); + } + }, this), this.pid); + } + }; /** @@ -2367,13 +7043,16 @@ */ RequestQueue.prototype.clear = function() { this.memQueue = []; - this.storage.removeItem(this.storageKey); + + if (this.usePersistence) { + this.storage.removeItem(this.storageKey); + } }; // maximum interval between request retries after exponential backoff var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - var logger = console_with_prefix('batch'); + var logger$1 = console_with_prefix('batch'); /** * RequestBatcher: manages the queueing, flushing, retry etc of requests of one @@ -2385,7 +7064,8 @@ this.errorReporter = options.errorReporter; this.queue = new RequestQueue(storageKey, { errorReporter: _.bind(this.reportError, this), - storage: options.storage + storage: options.storage, + usePersistence: options.usePersistence }); this.libConfig = options.libConfig; @@ -2402,6 +7082,11 @@ // extra client-side dedupe this.itemIdsSentSuccessfully = {}; + + // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes + // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up + // in a request loop and get ratelimited by the server. + this.flushOnlyOnInterval = options.flushOnlyOnInterval || false; }; /** @@ -2477,7 +7162,7 @@ try { if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); + logger$1.log('Flush: Request already in progress'); return; } @@ -2486,6 +7171,9 @@ var startTime = new Date().getTime(); var currentBatchSize = this.batchSize; var batch = this.queue.fillBatch(currentBatchSize); + // if there's more items in the queue than the batch size, attempt + // to flush again after the current batch is done. + var attemptSecondaryFlush = batch.length === currentBatchSize; var dataForRequest = []; var transformedItems = {}; _.each(batch, function(item) { @@ -2553,22 +7241,17 @@ this.flush(); } else if ( _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') + (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout') ) { // network or API error, or 429 Too Many Requests, retry var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } + if (res.retryAfter) { + retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS; } retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); this.reportError('Error; retry in ' + retryMS + ' ms'); this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { + } else if (_.isObject(res) && res.httpStatusCode === 413) { // 413 Payload Too Large if (batch.length > 1) { var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); @@ -2592,7 +7275,11 @@ _.bind(function(succeeded) { if (succeeded) { this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty + if (this.flushOnlyOnInterval && !attemptSecondaryFlush) { + this.resetFlush(); // schedule next batch with a delay + } else { + this.flush(); // handle next batch if the queue isn't empty + } } else { this.reportError('Failed to remove items from queue'); if (++this.consecutiveRemovalFailures > 5) { @@ -2638,9 +7325,8 @@ if (options.unloading) { requestOptions.transport = 'sendBeacon'; } - logger.log('MIXPANEL REQUEST:', dataForRequest); + logger$1.log('MIXPANEL REQUEST:', dataForRequest); this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - } catch(err) { this.reportError('Error flushing request queue', err); this.resetFlush(); @@ -2651,7 +7337,7 @@ * Log error to global logger and optional user-defined logger. */ RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); + logger$1.error.apply(logger$1.error, arguments); if (this.errorReporter) { try { if (!(err instanceof Error)) { @@ -2659,309 +7345,402 @@ } this.errorReporter(msg, err); } catch(err) { - logger.error(err); + logger$1.error(err); } } }; - /** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - - /** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ + var logger = console_with_prefix('recorder'); + var CompressionStream = win['CompressionStream']; - /** Public **/ + var RECORDER_BATCHER_LIB_CONFIG = { + 'batch_size': 1000, + 'batch_flush_interval_ms': 10 * 1000, + 'batch_request_timeout_ms': 90 * 1000, + 'batch_autostart': true + }; - var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; + var ACTIVE_SOURCES = new Set([ + IncrementalSource.MouseMove, + IncrementalSource.MouseInteraction, + IncrementalSource.Scroll, + IncrementalSource.ViewportResize, + IncrementalSource.Input, + IncrementalSource.TouchMove, + IncrementalSource.MediaInteraction, + IncrementalSource.Drag, + IncrementalSource.Selection, + ]); - /** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ - function optIn(token, options) { - _optInOut(true, token, options); + function isUserEvent(ev) { + return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.source); } - /** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ - function optOut(token, options) { - _optInOut(false, token, options); - } + var MixpanelRecorder = function(mixpanelInstance) { + this._mixpanel = mixpanelInstance; - /** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ - function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; - } + // internal rrweb stopRecording function + this._stopRecording = null; - /** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ - function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; + this.recEvents = []; + this.seqNo = 0; + this.replayId = null; + this.replayStartTime = null; + this.sendBatchId = null; + + this.idleTimeoutId = null; + this.maxTimeoutId = null; + + this.recordMaxMs = MAX_RECORDING_MS; + this._initBatcher(); + }; + + + MixpanelRecorder.prototype._initBatcher = function () { + this.batcher = new RequestBatcher('__mprec', { + libConfig: RECORDER_BATCHER_LIB_CONFIG, + sendRequestFunc: _.bind(this.flushEventsWithOptOut, this), + errorReporter: _.bind(this.reportError, this), + flushOnlyOnInterval: true, + usePersistence: false + }); + }; + + // eslint-disable-next-line camelcase + MixpanelRecorder.prototype.get_config = function(configVar) { + return this._mixpanel.get_config(configVar); + }; + + MixpanelRecorder.prototype.startRecording = function () { + if (this._stopRecording !== null) { + logger.log('Recording already in progress, skipping startRecording.'); + return; } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); + + this.recordMaxMs = this.get_config('record_max_ms'); + if (this.recordMaxMs > MAX_RECORDING_MS) { + this.recordMaxMs = MAX_RECORDING_MS; + logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.'); } - return optedOut; - } - /** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ - function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); - } + this.recEvents = []; + this.seqNo = 0; + this.replayStartTime = null; - /** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ - function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + this.replayId = _.UUID(); + + this.batcher.start(); + + var resetIdleTimeout = _.bind(function () { + clearTimeout(this.idleTimeoutId); + this.idleTimeoutId = setTimeout(_.bind(function () { + logger.log('Idle timeout reached, restarting recording.'); + this.resetRecording(); + }, this), this.get_config('record_idle_timeout_ms')); + }, this); + + this._stopRecording = record({ + 'emit': _.bind(function (ev) { + this.batcher.enqueue(ev); + if (isUserEvent(ev)) { + resetIdleTimeout(); + } + }, this), + 'blockClass': this.get_config('record_block_class'), + 'blockSelector': this.get_config('record_block_selector'), + 'collectFonts': this.get_config('record_collect_fonts'), + 'inlineImages': this.get_config('record_inline_images'), + 'maskAllInputs': true, + 'maskTextClass': this.get_config('record_mask_text_class'), + 'maskTextSelector': this.get_config('record_mask_text_selector') }); - } + + resetIdleTimeout(); + + this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs); + }; + + MixpanelRecorder.prototype.resetRecording = function () { + this.stopRecording(); + this.startRecording(); + }; + + MixpanelRecorder.prototype.stopRecording = function () { + if (this._stopRecording !== null) { + this._stopRecording(); + this._stopRecording = null; + } + + this.batcher.flush(); // flush any remaining events + this.replayId = null; + + clearTimeout(this.idleTimeoutId); + clearTimeout(this.maxTimeoutId); + }; /** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out + * Flushes the current batch of events to the server, but passes an opt-out callback to make sure + * we stop recording and dump any queued events if the user has opted out. */ - function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); + MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) { + this._flushEvents(data, options, cb, _.bind(this._onOptOut, this)); + }; + + MixpanelRecorder.prototype._onOptOut = function (code) { + // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out + if (code === 0) { + this.recEvents = []; + this.stopRecording(); + } + }; + + MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) { + var onSuccess = _.bind(function (response, responseBody) { + // Increment sequence counter only if the request was successful to guarantee ordering. + // RequestBatcher will always flush the next batch after the previous one succeeds. + if (response.status === 200) { + this.seqNo++; + } + + callback({ + status: 0, + httpStatusCode: response.status, + responseBody: responseBody, + retryAfter: response.headers.get('Retry-After') + }); + }, this); + + win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), { + 'method': 'POST', + 'headers': { + 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'), + 'Content-Type': 'application/octet-stream' + }, + 'body': reqBody, + }).then(function (response) { + response.json().then(function (responseBody) { + onSuccess(response, responseBody); + }).catch(function (error) { + callback({error: error}); + }); + }).catch(function (error) { + callback({error: error}); }); - } + }; - /** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ - function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); - } + MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) { + const numEvents = data.length; + + if (numEvents > 0) { + // each rrweb event has a timestamp - leverage those to get time properties + var batchStartTime = data[0].timestamp; + if (this.seqNo === 0) { + this.replayStartTime = batchStartTime; + } + var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime; + + var reqParams = { + 'distinct_id': String(this._mixpanel.get_distinct_id()), + 'seq': this.seqNo, + 'batch_start_time': batchStartTime / 1000, + 'replay_id': this.replayId, + 'replay_length_ms': replayLengthMs, + 'replay_start_time': this.replayStartTime / 1000 + }; + var eventsJson = _.JSONEncode(data); + + // send ID management props if they exist + var deviceId = this._mixpanel.get_property('$device_id'); + if (deviceId) { + reqParams['$device_id'] = deviceId; + } + var userId = this._mixpanel.get_property('$user_id'); + if (userId) { + reqParams['$user_id'] = userId; + } + + if (CompressionStream) { + var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream(); + var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip')); + new Response(gzipStream) + .blob() + .then(_.bind(function(compressedBlob) { + reqParams['format'] = 'gzip'; + this._sendRequest(reqParams, compressedBlob, callback); + }, this)); + } else { + reqParams['format'] = 'body'; + this._sendRequest(reqParams, eventsJson, callback); + } + } + }); + + + MixpanelRecorder.prototype.reportError = function(msg, err) { + logger.error.apply(logger.error, arguments); + try { + if (!err && !(msg instanceof Error)) { + msg = new Error(msg); + } + this.get_config('error_reporter')(msg, err); + } catch(err) { + logger.error(err); + } + }; - /** Private **/ - /** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ - function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; - } + win['__mp_recorder'] = MixpanelRecorder; - /** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ - function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; - } + /* eslint camelcase: "off" */ /** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type + * DomTracker Object + * @constructor */ - function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); - } + var DomTracker = function() {}; - /** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ - function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); + // interface + DomTracker.prototype.create_properties = function() {}; + DomTracker.prototype.event_handler = function() {}; + DomTracker.prototype.after_track_handler = function() {}; - return hasDntOn; - } + DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; + }; /** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function=} user_callback */ - function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); + DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); return; } - options = options || {}; + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); + that.event_handler(e, this, options); - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true + // in case the mixpanel servers don't get back to us in time + window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); }); - } - } + }, this); + + return true; + }; /** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out + * @param {function} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured */ - function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; + DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; } - if (!optedOut) { - return method.apply(this, arguments); - } + that.after_track_handler(props, options, timeout_occured); + }; + }; - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } + DomTracker.prototype.create_properties = function(properties, element) { + var props; - return; - }; - } + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; + }; + + /** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ + var LinkTracker = function() { + this.override_event = 'click'; + }; + _.inherit(LinkTracker, DomTracker); + + LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; + }; + + LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } + }; + + LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + window.location = options.href; + }, 0); + }; + + /** + * FormTracker Object + * @constructor + * @extends DomTracker + */ + var FormTracker = function() { + this.override_event = 'submit'; + }; + _.inherit(FormTracker, DomTracker); + + FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); + }; + + FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); + }; /* eslint camelcase: "off" */ @@ -3383,7 +8162,7 @@ _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); return; } else { $add[k] = v; @@ -3508,7 +8287,7 @@ if (!_.isNumber(amount)) { amount = parseFloat(amount); if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } @@ -3545,7 +8324,7 @@ */ MixpanelPeople.prototype.delete_user = function() { if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); return; } var data = {'$delete': this._mixpanel.get_distinct_id()}; @@ -3619,7 +8398,7 @@ } else if (UNION_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); } else { - console.error('Invalid call to _enqueue():', data); + console$1.error('Invalid call to _enqueue():', data); } }; @@ -3767,7 +8546,7 @@ var storage_type = config['persistence']; if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); storage_type = config['persistence'] = 'cookie'; } @@ -4071,8 +8850,8 @@ this._pop_from_people_queue(UNSET_ACTION, q_data); } - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); this.save(); }; @@ -4117,7 +8896,7 @@ } else if (queue === UNION_ACTION) { return UNION_QUEUE_KEY; } else { - console.error('Invalid queue:', queue); + console$1.error('Invalid queue:', queue); } }; @@ -4174,6 +8953,12 @@ */ var init_type; // MODULE or SNIPPET loader + // allow bundlers to specify how extra code (recorder bundle) should be loaded + // eslint-disable-next-line no-unused-vars + var load_extra_bundle = function(src, _onload) { + throw new Error(src + ' not available in this build.'); + }; + var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; @@ -4267,7 +9052,9 @@ 'hooks': {}, 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), 'record_block_selector': 'img, video', + 'record_collect_fonts': false, 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes + 'record_inline_images': false, 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), 'record_mask_text_selector': '*', 'record_max_ms': MAX_RECORDING_MS, @@ -4300,7 +9087,7 @@ instance = target; } else { if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); + console$1.error('You have already initialized ' + name); return; } instance = new MixpanelLib(); @@ -4428,9 +9215,9 @@ if (this._batch_requests) { if (!_.localStorage.is_supported(true) || !USE_XHR) { this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); + console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); + console$1.log('Clearing batch queue ' + batcher_config.queue_key); _.localStorage.remove(batcher_config.queue_key); }); } else { @@ -4493,7 +9280,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); + console$1.critical('Browser does not support MutationObserver; skipping session recording'); return; } @@ -4503,12 +9290,7 @@ }, this); if (_.isUndefined(win['__mp_recorder'])) { - var scriptEl = document$1.createElement('script'); - scriptEl.type = 'text/javascript'; - scriptEl.async = true; - scriptEl.onload = handleLoadedRecorder; - scriptEl.src = this.get_config('recorder_src'); - document$1.head.appendChild(scriptEl); + load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); } else { handleLoadedRecorder(); } @@ -4518,7 +9300,7 @@ if (this._recorder) { this._recorder['stopRecording'](); } else { - console.critical('Session recorder module not loaded'); + console$1.critical('Session recorder module not loaded'); } }; @@ -4807,7 +9589,8 @@ lib.report_error(error); if (callback) { if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); + var response_headers = req['responseHeaders'] || {}; + callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']}); } else { callback(0); } @@ -4907,6 +9690,7 @@ attrs.queue_key, { libConfig: this['config'], + errorReporter: this.get_config('error_reporter'), sendRequestFunc: _.bind(function(data, options, cb) { this._send_request( this.get_config('api_host') + attrs.endpoint, @@ -4918,8 +9702,8 @@ beforeSendHook: _.bind(function(item) { return this._run_hook('before_send_' + attrs.type, item); }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) + stopAllBatchingFunc: _.bind(this.stop_batch_senders, this), + usePersistence: true } ); }, this); @@ -5011,8 +9795,8 @@ truncated_data = this._run_hook('before_send_' + options.type, truncated_data); } if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); return this._send_request( endpoint, this._encode_data_for_request(truncated_data), @@ -6209,14 +10993,14 @@ }; MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); + console$1.error.apply(console$1.error, arguments); try { if (!err && !(msg instanceof Error)) { msg = new Error(msg); } this.get_config('error_reporter')(msg, err); } catch(err) { - console.error(err); + console$1.error(err); } }; @@ -6368,7 +11152,8 @@ _.register_event(win, 'load', dom_loaded_handler, true); }; - function init_as_module() { + function init_as_module(bundle_loader) { + load_extra_bundle = bundle_loader; init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); @@ -6379,9 +11164,16 @@ return mixpanel_master; } + // For loading separate bundles asynchronously via script tag + + // For builds that have everything in one bundle, no extra work. + function loadNoop (_src, onload) { + onload(); + } + /* eslint camelcase: "off" */ - var mixpanel = init_as_module(); + var mixpanel = init_as_module(loadNoop); return mixpanel; diff --git a/package-lock.json b/package-lock.json index 0195ee3c..c6426011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "request": "2.88.0", "rollup": "2.79.1", "rollup-plugin-esbuild": "4.10.3", - "rollup-plugin-npm": "1.4.0", "sinon": "8.1.1", "sinon-chai": "3.5.0", "webpack": "1.12.2" @@ -3522,15 +3521,6 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, - "node_modules/builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/builtins": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/builtins/-/builtins-0.0.7.tgz", @@ -11031,18 +11021,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/rollup-plugin-npm": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-npm/-/rollup-plugin-npm-1.4.0.tgz", - "integrity": "sha1-ip+xpKA4vKRV58ImDxQwDyp18WQ=", - "deprecated": "rollup-plugin-npm has been renamed to rollup-plugin-node-resolve", - "dev": true, - "dependencies": { - "browser-resolve": "^1.11.0", - "builtin-modules": "^1.1.0", - "resolve": "^1.1.6" - } - }, "node_modules/rollup/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -16526,12 +16504,6 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, "builtins": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/builtins/-/builtins-0.0.7.tgz", @@ -22626,17 +22598,6 @@ } } }, - "rollup-plugin-npm": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-npm/-/rollup-plugin-npm-1.4.0.tgz", - "integrity": "sha1-ip+xpKA4vKRV58ImDxQwDyp18WQ=", - "dev": true, - "requires": { - "browser-resolve": "^1.11.0", - "builtin-modules": "^1.1.0", - "resolve": "^1.1.6" - } - }, "rrdom": { "version": "2.0.0-alpha.13", "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.0.0-alpha.13.tgz", diff --git a/src/config.js b/src/config.js index d76bcb1b..81500b1d 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,6 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.53.0' + LIB_VERSION: '2.54.0-rc1' }; export default Config; diff --git a/tests/test.js b/tests/test.js index 4968b7bb..33c5b9e4 100644 --- a/tests/test.js +++ b/tests/test.js @@ -5547,7 +5547,7 @@ asyncTest('retries record request after a 500', 17, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); - ok(this.getRecorderScript() !== null); + this.assertRecorderScript(true) // fake the fetch / response promises since we're testing callback logic this.responseBlobStub = sinon.stub(window.Response.prototype, 'blob'); @@ -5563,18 +5563,18 @@ same(this.fetchStub.getCalls().length, 1, 'one batch fetch request made every ten seconds'); var urlParams = validateAndGetUrlParams(this.fetchStub.getCall(0)); - same(urlParams.get("seq"), "0"); + same(urlParams.get("seq"), "0", "sends first sequence"); simulateMouseClick(document.body); this.clock.tick(10 * 1000); same(this.fetchStub.getCalls().length, 2, 'one batch fetch request made every ten seconds'); urlParams = validateAndGetUrlParams(this.fetchStub.getCall(1)); - same(urlParams.get("seq"), "1"); + same(urlParams.get("seq"), "1", "2nd sequence fails"); this.clock.tick(20 * 2000); same(this.fetchStub.getCalls().length, 3, 'record request is retried after a 500'); validateAndGetUrlParams(this.fetchStub.getCall(2)); - same(urlParams.get("seq"), "1"); + same(urlParams.get("seq"), "1", "2nd sequence is retried"); mixpanel.recordertest.stop_session_recording(); }); @@ -5583,7 +5583,7 @@ asyncTest('halves batch size and retries record request after a 413', 25, function () { this.randomStub.returns(0.02); this.initMixpanelRecorder({record_sessions_percent: 10}); - ok(this.getRecorderScript() !== null); + this.assertRecorderScript(true) this.randomStub.restore(); // restore the random stub after script is loaded for batcher uuid dedupe this.blobConstructorSpy = sinon.spy(window, 'Blob') From 31d8461d3c73381d91ebbb1b11e2ab7780aa3e2f Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 23 Jul 2024 21:35:50 +0000 Subject: [PATCH 46/48] rebuild --- dist/mixpanel-core.cjs.js | 2 +- dist/mixpanel-main.cjs.js | 6330 ---------------------- dist/mixpanel-recorder.js | 2 +- dist/mixpanel-recorder.min.js | 2 +- dist/mixpanel-with-async-recorder.cjs.js | 2 +- dist/mixpanel.amd.js | 2 +- dist/mixpanel.cjs.js | 2 +- dist/mixpanel.globals.js | 2 +- dist/mixpanel.min.js | 4 +- dist/mixpanel.umd.js | 2 +- examples/commonjs-browserify/bundle.js | 2 +- examples/es2015-babelify/bundle.js | 2 +- examples/umd-webpack/bundle.js | 2 +- src/config.js | 2 +- 14 files changed, 14 insertions(+), 6344 deletions(-) delete mode 100644 dist/mixpanel-main.cjs.js diff --git a/dist/mixpanel-core.cjs.js b/dist/mixpanel-core.cjs.js index dfd0ef23..3befceef 100644 --- a/dist/mixpanel-core.cjs.js +++ b/dist/mixpanel-core.cjs.js @@ -2,7 +2,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; /* eslint camelcase: "off", eqeqeq: "off" */ diff --git a/dist/mixpanel-main.cjs.js b/dist/mixpanel-main.cjs.js deleted file mode 100644 index 128f89bd..00000000 --- a/dist/mixpanel-main.cjs.js +++ /dev/null @@ -1,6330 +0,0 @@ -'use strict'; - -var Config = { - DEBUG: false, - LIB_VERSION: '2.53.0' -}; - -/* eslint camelcase: "off", eqeqeq: "off" */ - -// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file -var win; -if (typeof(window) === 'undefined') { - var loc = { - hostname: '' - }; - win = { - navigator: { userAgent: '' }, - document: { - location: loc, - referrer: '' - }, - screen: { width: 0, height: 0 }, - location: loc - }; -} else { - win = window; -} - -// Maximum allowed session recording length -var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours - -/* - * Saved references to long variable names, so that closure compiler can - * minimize file size. - */ - -var ArrayProto = Array.prototype, - FuncProto = Function.prototype, - ObjProto = Object.prototype, - slice = ArrayProto.slice, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty, - windowConsole = win.console, - navigator = win.navigator, - document$1 = win.document, - windowOpera = win.opera, - screen = win.screen, - userAgent = navigator.userAgent; - -var nativeBind = FuncProto.bind, - nativeForEach = ArrayProto.forEach, - nativeIndexOf = ArrayProto.indexOf, - nativeMap = ArrayProto.map, - nativeIsArray = Array.isArray, - breaker = {}; - -var _ = { - trim: function(str) { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill - return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); - } -}; - -// Console override -var console = { - /** @type {function(...*)} */ - log: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - try { - windowConsole.log.apply(windowConsole, arguments); - } catch (err) { - _.each(arguments, function(arg) { - windowConsole.log(arg); - }); - } - } - }, - /** @type {function(...*)} */ - warn: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); - try { - windowConsole.warn.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.warn(arg); - }); - } - } - }, - /** @type {function(...*)} */ - error: function() { - if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } - }, - /** @type {function(...*)} */ - critical: function() { - if (!_.isUndefined(windowConsole) && windowConsole) { - var args = ['Mixpanel error:'].concat(_.toArray(arguments)); - try { - windowConsole.error.apply(windowConsole, args); - } catch (err) { - _.each(args, function(arg) { - windowConsole.error(arg); - }); - } - } - } -}; - -var log_func_with_prefix = function(func, prefix) { - return function() { - arguments[0] = '[' + prefix + '] ' + arguments[0]; - return func.apply(console, arguments); - }; -}; -var console_with_prefix = function(prefix) { - return { - log: log_func_with_prefix(console.log, prefix), - error: log_func_with_prefix(console.error, prefix), - critical: log_func_with_prefix(console.critical, prefix) - }; -}; - - -// UNDERSCORE -// Embed part of the Underscore Library -_.bind = function(func, context) { - var args, bound; - if (nativeBind && func.bind === nativeBind) { - return nativeBind.apply(func, slice.call(arguments, 1)); - } - if (!_.isFunction(func)) { - throw new TypeError(); - } - args = slice.call(arguments, 2); - bound = function() { - if (!(this instanceof bound)) { - return func.apply(context, args.concat(slice.call(arguments))); - } - var ctor = {}; - ctor.prototype = func.prototype; - var self = new ctor(); - ctor.prototype = null; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) { - return result; - } - return self; - }; - return bound; -}; - -/** - * @param {*=} obj - * @param {function(...*)=} iterator - * @param {Object=} context - */ -_.each = function(obj, iterator, context) { - if (obj === null || obj === undefined) { - return; - } - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { - return; - } - } - } else { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) { - return; - } - } - } - } -}; - -_.extend = function(obj) { - _.each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (source[prop] !== void 0) { - obj[prop] = source[prop]; - } - } - }); - return obj; -}; - -_.isArray = nativeIsArray || function(obj) { - return toString.call(obj) === '[object Array]'; -}; - -// from a comment on http://dbj.org/dbj/?p=286 -// fails on only one very rare and deliberate custom object: -// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; -_.isFunction = function(f) { - try { - return /^\s*\bfunction\b/.test(f); - } catch (x) { - return false; - } -}; - -_.isArguments = function(obj) { - return !!(obj && hasOwnProperty.call(obj, 'callee')); -}; - -_.toArray = function(iterable) { - if (!iterable) { - return []; - } - if (iterable.toArray) { - return iterable.toArray(); - } - if (_.isArray(iterable)) { - return slice.call(iterable); - } - if (_.isArguments(iterable)) { - return slice.call(iterable); - } - return _.values(iterable); -}; - -_.map = function(arr, callback, context) { - if (nativeMap && arr.map === nativeMap) { - return arr.map(callback, context); - } else { - var results = []; - _.each(arr, function(item) { - results.push(callback.call(context, item)); - }); - return results; - } -}; - -_.keys = function(obj) { - var results = []; - if (obj === null) { - return results; - } - _.each(obj, function(value, key) { - results[results.length] = key; - }); - return results; -}; - -_.values = function(obj) { - var results = []; - if (obj === null) { - return results; - } - _.each(obj, function(value) { - results[results.length] = value; - }); - return results; -}; - -_.include = function(obj, target) { - var found = false; - if (obj === null) { - return found; - } - if (nativeIndexOf && obj.indexOf === nativeIndexOf) { - return obj.indexOf(target) != -1; - } - _.each(obj, function(value) { - if (found || (found = (value === target))) { - return breaker; - } - }); - return found; -}; - -_.includes = function(str, needle) { - return str.indexOf(needle) !== -1; -}; - -// Underscore Addons -_.inherit = function(subclass, superclass) { - subclass.prototype = new superclass(); - subclass.prototype.constructor = subclass; - subclass.superclass = superclass.prototype; - return subclass; -}; - -_.isObject = function(obj) { - return (obj === Object(obj) && !_.isArray(obj)); -}; - -_.isEmptyObject = function(obj) { - if (_.isObject(obj)) { - for (var key in obj) { - if (hasOwnProperty.call(obj, key)) { - return false; - } - } - return true; - } - return false; -}; - -_.isUndefined = function(obj) { - return obj === void 0; -}; - -_.isString = function(obj) { - return toString.call(obj) == '[object String]'; -}; - -_.isDate = function(obj) { - return toString.call(obj) == '[object Date]'; -}; - -_.isNumber = function(obj) { - return toString.call(obj) == '[object Number]'; -}; - -_.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); -}; - -_.encodeDates = function(obj) { - _.each(obj, function(v, k) { - if (_.isDate(v)) { - obj[k] = _.formatDate(v); - } else if (_.isObject(v)) { - obj[k] = _.encodeDates(v); // recurse - } - }); - return obj; -}; - -_.timestamp = function() { - Date.now = Date.now || function() { - return +new Date; - }; - return Date.now(); -}; - -_.formatDate = function(d) { - // YYYY-MM-DDTHH:MM:SS in UTC - function pad(n) { - return n < 10 ? '0' + n : n; - } - return d.getUTCFullYear() + '-' + - pad(d.getUTCMonth() + 1) + '-' + - pad(d.getUTCDate()) + 'T' + - pad(d.getUTCHours()) + ':' + - pad(d.getUTCMinutes()) + ':' + - pad(d.getUTCSeconds()); -}; - -_.strip_empty_properties = function(p) { - var ret = {}; - _.each(p, function(v, k) { - if (_.isString(v) && v.length > 0) { - ret[k] = v; - } - }); - return ret; -}; - -/* - * this function returns a copy of object after truncating it. If - * passed an Array or Object it will iterate through obj and - * truncate all the values recursively. - */ -_.truncate = function(obj, length) { - var ret; - - if (typeof(obj) === 'string') { - ret = obj.slice(0, length); - } else if (_.isArray(obj)) { - ret = []; - _.each(obj, function(val) { - ret.push(_.truncate(val, length)); - }); - } else if (_.isObject(obj)) { - ret = {}; - _.each(obj, function(val, key) { - ret[key] = _.truncate(val, length); - }); - } else { - ret = obj; - } - - return ret; -}; - -_.JSONEncode = (function() { - return function(mixed_val) { - var value = mixed_val; - var quote = function(string) { - var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex - var meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"': '\\"', - '\\': '\\\\' - }; - - escapable.lastIndex = 0; - return escapable.test(string) ? - '"' + string.replace(escapable, function(a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : - '"' + string + '"'; - }; - - var str = function(key, holder) { - var gap = ''; - var indent = ' '; - var i = 0; // The loop counter. - var k = ''; // The member key. - var v = ''; // The member value. - var length = 0; - var mind = gap; - var partial = []; - var value = holder[key]; - - // If the value has a toJSON method, call it to obtain a replacement value. - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - - // What happens next depends on the value's type. - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - // JSON numbers must be finite. Encode non-finite numbers as null. - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - // If the value is a boolean or null, convert it to a string. Note: - // typeof null does not produce 'null'. The case is included here in - // the remote chance that this gets fixed someday. - - return String(value); - - case 'object': - // If the type is 'object', we might be dealing with an object or an array or - // null. - // Due to a specification blunder in ECMAScript, typeof null is 'object', - // so watch out for that case. - if (!value) { - return 'null'; - } - - // Make an array to hold the partial results of stringifying this object value. - gap += indent; - partial = []; - - // Is the value an array? - if (toString.apply(value) === '[object Array]') { - // The value is an array. Stringify every element. Use null as a placeholder - // for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - - // Join all of the elements together, separated with commas, and wrap them in - // brackets. - v = partial.length === 0 ? '[]' : - gap ? '[\n' + gap + - partial.join(',\n' + gap) + '\n' + - mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - - // Iterate through all of the keys in the object. - for (k in value) { - if (hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - - // Join all of the member texts together, separated with commas, - // and wrap them in braces. - v = partial.length === 0 ? '{}' : - gap ? '{' + partial.join(',') + '' + - mind + '}' : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - }; - - // Make a fake root object containing our value under the key of ''. - // Return the result of stringifying the value. - return str('', { - '': value - }); - }; -})(); - -/** - * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js - * Slightly modified to throw a real Error rather than a POJO - */ -_.JSONDecode = (function() { - var at, // The index of the current character - ch, // The current character - escapee = { - '"': '"', - '\\': '\\', - '/': '/', - 'b': '\b', - 'f': '\f', - 'n': '\n', - 'r': '\r', - 't': '\t' - }, - text, - error = function(m) { - var e = new SyntaxError(m); - e.at = at; - e.text = text; - throw e; - }, - next = function(c) { - // If a c parameter is provided, verify that it matches the current character. - if (c && c !== ch) { - error('Expected \'' + c + '\' instead of \'' + ch + '\''); - } - // Get the next character. When there are no more characters, - // return the empty string. - ch = text.charAt(at); - at += 1; - return ch; - }, - number = function() { - // Parse a number value. - var number, - string = ''; - - if (ch === '-') { - string = '-'; - next('-'); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - if (ch === '.') { - string += '.'; - while (next() && ch >= '0' && ch <= '9') { - string += ch; - } - } - if (ch === 'e' || ch === 'E') { - string += ch; - next(); - if (ch === '-' || ch === '+') { - string += ch; - next(); - } - while (ch >= '0' && ch <= '9') { - string += ch; - next(); - } - } - number = +string; - if (!isFinite(number)) { - error('Bad number'); - } else { - return number; - } - }, - - string = function() { - // Parse a string value. - var hex, - i, - string = '', - uffff; - // When parsing for string values, we must look for " and \ characters. - if (ch === '"') { - while (next()) { - if (ch === '"') { - next(); - return string; - } - if (ch === '\\') { - next(); - if (ch === 'u') { - uffff = 0; - for (i = 0; i < 4; i += 1) { - hex = parseInt(next(), 16); - if (!isFinite(hex)) { - break; - } - uffff = uffff * 16 + hex; - } - string += String.fromCharCode(uffff); - } else if (typeof escapee[ch] === 'string') { - string += escapee[ch]; - } else { - break; - } - } else { - string += ch; - } - } - } - error('Bad string'); - }, - white = function() { - // Skip whitespace. - while (ch && ch <= ' ') { - next(); - } - }, - word = function() { - // true, false, or null. - switch (ch) { - case 't': - next('t'); - next('r'); - next('u'); - next('e'); - return true; - case 'f': - next('f'); - next('a'); - next('l'); - next('s'); - next('e'); - return false; - case 'n': - next('n'); - next('u'); - next('l'); - next('l'); - return null; - } - error('Unexpected "' + ch + '"'); - }, - value, // Placeholder for the value function. - array = function() { - // Parse an array value. - var array = []; - - if (ch === '[') { - next('['); - white(); - if (ch === ']') { - next(']'); - return array; // empty array - } - while (ch) { - array.push(value()); - white(); - if (ch === ']') { - next(']'); - return array; - } - next(','); - white(); - } - } - error('Bad array'); - }, - object = function() { - // Parse an object value. - var key, - object = {}; - - if (ch === '{') { - next('{'); - white(); - if (ch === '}') { - next('}'); - return object; // empty object - } - while (ch) { - key = string(); - white(); - next(':'); - if (Object.hasOwnProperty.call(object, key)) { - error('Duplicate key "' + key + '"'); - } - object[key] = value(); - white(); - if (ch === '}') { - next('}'); - return object; - } - next(','); - white(); - } - } - error('Bad object'); - }; - - value = function() { - // Parse a JSON value. It could be an object, an array, a string, - // a number, or a word. - white(); - switch (ch) { - case '{': - return object(); - case '[': - return array(); - case '"': - return string(); - case '-': - return number(); - default: - return ch >= '0' && ch <= '9' ? number() : word(); - } - }; - - // Return the json_parse function. It will have access to all of the - // above functions and variables. - return function(source) { - var result; - - text = source; - at = 0; - ch = ' '; - result = value(); - white(); - if (ch) { - error('Syntax error'); - } - - return result; - }; -})(); - -_.base64Encode = function(data) { - var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, - ac = 0, - enc = '', - tmp_arr = []; - - if (!data) { - return data; - } - - data = _.utf8Encode(data); - - do { // pack three octets into four hexets - o1 = data.charCodeAt(i++); - o2 = data.charCodeAt(i++); - o3 = data.charCodeAt(i++); - - bits = o1 << 16 | o2 << 8 | o3; - - h1 = bits >> 18 & 0x3f; - h2 = bits >> 12 & 0x3f; - h3 = bits >> 6 & 0x3f; - h4 = bits & 0x3f; - - // use hexets to index into b64, and append result to encoded string - tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); - } while (i < data.length); - - enc = tmp_arr.join(''); - - switch (data.length % 3) { - case 1: - enc = enc.slice(0, -2) + '=='; - break; - case 2: - enc = enc.slice(0, -1) + '='; - break; - } - - return enc; -}; - -_.utf8Encode = function(string) { - string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - var utftext = '', - start, - end; - var stringl = 0, - n; - - start = end = 0; - stringl = string.length; - - for (n = 0; n < stringl; n++) { - var c1 = string.charCodeAt(n); - var enc = null; - - if (c1 < 128) { - end++; - } else if ((c1 > 127) && (c1 < 2048)) { - enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); - } else { - enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); - } - if (enc !== null) { - if (end > start) { - utftext += string.substring(start, end); - } - utftext += enc; - start = end = n + 1; - } - } - - if (end > start) { - utftext += string.substring(start, string.length); - } - - return utftext; -}; - -_.UUID = (function() { - - // Time-based entropy - var T = function() { - var time = 1 * new Date(); // cross-browser version of Date.now() - var ticks; - if (win.performance && win.performance.now) { - ticks = win.performance.now(); - } else { - // fall back to busy loop - ticks = 0; - - // this while loop figures how many browser ticks go by - // before 1*new Date() returns a new number, ie the amount - // of ticks that go by per millisecond - while (time == 1 * new Date()) { - ticks++; - } - } - return time.toString(16) + Math.floor(ticks).toString(16); - }; - - // Math.Random entropy - var R = function() { - return Math.random().toString(16).replace('.', ''); - }; - - // User agent entropy - // This function takes the user agent string, and then xors - // together each sequence of 8 bytes. This produces a final - // sequence of 8 bytes which it returns as hex. - var UA = function() { - var ua = userAgent, - i, ch, buffer = [], - ret = 0; - - function xor(result, byte_array) { - var j, tmp = 0; - for (j = 0; j < byte_array.length; j++) { - tmp |= (buffer[j] << j * 8); - } - return result ^ tmp; - } - - for (i = 0; i < ua.length; i++) { - ch = ua.charCodeAt(i); - buffer.unshift(ch & 0xFF); - if (buffer.length >= 4) { - ret = xor(ret, buffer); - buffer = []; - } - } - - if (buffer.length > 0) { - ret = xor(ret, buffer); - } - - return ret.toString(16); - }; - - return function() { - var se = (screen.height * screen.width).toString(16); - return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); - }; -})(); - -// _.isBlockedUA() -// This is to block various web spiders from executing our JS and -// sending false tracking data -var BLOCKED_UA_STRS = [ - 'ahrefsbot', - 'ahrefssiteaudit', - 'baiduspider', - 'bingbot', - 'bingpreview', - 'chrome-lighthouse', - 'facebookexternal', - 'petalbot', - 'pinterest', - 'screaming frog', - 'yahoo! slurp', - 'yandexbot', - - // a whole bunch of goog-specific crawlers - // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers - 'adsbot-google', - 'apis-google', - 'duplexweb-google', - 'feedfetcher-google', - 'google favicon', - 'google web preview', - 'google-read-aloud', - 'googlebot', - 'googleweblight', - 'mediapartners-google', - 'storebot-google' -]; -_.isBlockedUA = function(ua) { - var i; - ua = ua.toLowerCase(); - for (i = 0; i < BLOCKED_UA_STRS.length; i++) { - if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { - return true; - } - } - return false; -}; - -/** - * @param {Object=} formdata - * @param {string=} arg_separator - */ -_.HTTPBuildQuery = function(formdata, arg_separator) { - var use_val, use_key, tmp_arr = []; - - if (_.isUndefined(arg_separator)) { - arg_separator = '&'; - } - - _.each(formdata, function(val, key) { - use_val = encodeURIComponent(val.toString()); - use_key = encodeURIComponent(key); - tmp_arr[tmp_arr.length] = use_key + '=' + use_val; - }); - - return tmp_arr.join(arg_separator); -}; - -_.getQueryParam = function(url, param) { - // Expects a raw URL - - param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); - var regexS = '[\\?&]' + param + '=([^&#]*)', - regex = new RegExp(regexS), - results = regex.exec(url); - if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { - return ''; - } else { - var result = results[1]; - try { - result = decodeURIComponent(result); - } catch(err) { - console.error('Skipping decoding for malformed query param: ' + result); - } - return result.replace(/\+/g, ' '); - } -}; - - -// _.cookie -// Methods partially borrowed from quirksmode.org/js/cookies.html -_.cookie = { - get: function(name) { - var nameEQ = name + '='; - var ca = document$1.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1, c.length); - } - if (c.indexOf(nameEQ) === 0) { - return decodeURIComponent(c.substring(nameEQ.length, c.length)); - } - } - return null; - }, - - parse: function(name) { - var cookie; - try { - cookie = _.JSONDecode(_.cookie.get(name)) || {}; - } catch (err) { - // noop - } - return cookie; - }, - - set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { - var cdomain = '', - expires = '', - secure = ''; - - if (domain_override) { - cdomain = '; domain=' + domain_override; - } else if (is_cross_subdomain) { - var domain = extract_domain(document$1.location.hostname); - cdomain = domain ? '; domain=.' + domain : ''; - } - - if (seconds) { - var date = new Date(); - date.setTime(date.getTime() + (seconds * 1000)); - expires = '; expires=' + date.toGMTString(); - } - - if (is_cross_site) { - is_secure = true; - secure = '; SameSite=None'; - } - if (is_secure) { - secure += '; secure'; - } - - document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; - }, - - set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { - var cdomain = '', expires = '', secure = ''; - - if (domain_override) { - cdomain = '; domain=' + domain_override; - } else if (is_cross_subdomain) { - var domain = extract_domain(document$1.location.hostname); - cdomain = domain ? '; domain=.' + domain : ''; - } - - if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = '; expires=' + date.toGMTString(); - } - - if (is_cross_site) { - is_secure = true; - secure = '; SameSite=None'; - } - if (is_secure) { - secure += '; secure'; - } - - var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; - document$1.cookie = new_cookie_val; - return new_cookie_val; - }, - - remove: function(name, is_cross_subdomain, domain_override) { - _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); - } -}; - -var _localStorageSupported = null; -var localStorageSupported = function(storage, forceCheck) { - if (_localStorageSupported !== null && !forceCheck) { - return _localStorageSupported; - } - - var supported = true; - try { - storage = storage || window.localStorage; - var key = '__mplss_' + cheap_guid(8), - val = 'xyz'; - storage.setItem(key, val); - if (storage.getItem(key) !== val) { - supported = false; - } - storage.removeItem(key); - } catch (err) { - supported = false; - } - - _localStorageSupported = supported; - return supported; -}; - -// _.localStorage -_.localStorage = { - is_supported: function(force_check) { - var supported = localStorageSupported(null, force_check); - if (!supported) { - console.error('localStorage unsupported; falling back to cookie store'); - } - return supported; - }, - - error: function(msg) { - console.error('localStorage error: ' + msg); - }, - - get: function(name) { - try { - return window.localStorage.getItem(name); - } catch (err) { - _.localStorage.error(err); - } - return null; - }, - - parse: function(name) { - try { - return _.JSONDecode(_.localStorage.get(name)) || {}; - } catch (err) { - // noop - } - return null; - }, - - set: function(name, value) { - try { - window.localStorage.setItem(name, value); - } catch (err) { - _.localStorage.error(err); - } - }, - - remove: function(name) { - try { - window.localStorage.removeItem(name); - } catch (err) { - _.localStorage.error(err); - } - } -}; - -_.register_event = (function() { - // written by Dean Edwards, 2005 - // with input from Tino Zijdel - crisp@xs4all.nl - // with input from Carl Sverre - mail@carlsverre.com - // with input from Mixpanel - // http://dean.edwards.name/weblog/2005/10/add-event/ - // https://gist.github.com/1930440 - - /** - * @param {Object} element - * @param {string} type - * @param {function(...*)} handler - * @param {boolean=} oldSchool - * @param {boolean=} useCapture - */ - var register_event = function(element, type, handler, oldSchool, useCapture) { - if (!element) { - console.error('No valid element provided to register_event'); - return; - } - - if (element.addEventListener && !oldSchool) { - element.addEventListener(type, handler, !!useCapture); - } else { - var ontype = 'on' + type; - var old_handler = element[ontype]; // can be undefined - element[ontype] = makeHandler(element, handler, old_handler); - } - }; - - function makeHandler(element, new_handler, old_handlers) { - var handler = function(event) { - event = event || fixEvent(window.event); - - // this basically happens in firefox whenever another script - // overwrites the onload callback and doesn't pass the event - // object to previously defined callbacks. All the browsers - // that don't define window.event implement addEventListener - // so the dom_loaded handler will still be fired as usual. - if (!event) { - return undefined; - } - - var ret = true; - var old_result, new_result; - - if (_.isFunction(old_handlers)) { - old_result = old_handlers(event); - } - new_result = new_handler.call(element, event); - - if ((false === old_result) || (false === new_result)) { - ret = false; - } - - return ret; - }; - - return handler; - } - - function fixEvent(event) { - if (event) { - event.preventDefault = fixEvent.preventDefault; - event.stopPropagation = fixEvent.stopPropagation; - } - return event; - } - fixEvent.preventDefault = function() { - this.returnValue = false; - }; - fixEvent.stopPropagation = function() { - this.cancelBubble = true; - }; - - return register_event; -})(); - - -var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); - -_.dom_query = (function() { - /* document.getElementsBySelector(selector) - - returns an array of element objects from the current document - matching the CSS selector. Selectors can contain element names, - class names and ids and can be nested. For example: - - elements = document.getElementsBySelector('div#main p a.external') - - Will return an array of all 'a' elements with 'external' in their - class attribute that are contained inside 'p' elements that are - contained inside the 'div' element which has id="main" - - New in version 0.4: Support for CSS2 and CSS3 attribute selectors: - See http://www.w3.org/TR/css3-selectors/#attribute-selectors - - Version 0.4 - Simon Willison, March 25th 2003 - -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows - -- Opera 7 fails - - Version 0.5 - Carl Sverre, Jan 7th 2013 - -- Now uses jQuery-esque `hasClass` for testing class name - equality. This fixes a bug related to '-' characters being - considered not part of a 'word' in regex. - */ - - function getAllChildren(e) { - // Returns all children of element. Workaround required for IE5/Windows. Ugh. - return e.all ? e.all : e.getElementsByTagName('*'); - } - - var bad_whitespace = /[\t\r\n]/g; - - function hasClass(elem, selector) { - var className = ' ' + selector + ' '; - return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); - } - - function getElementsBySelector(selector) { - // Attempt to fail gracefully in lesser browsers - if (!document$1.getElementsByTagName) { - return []; - } - // Split selector in to tokens - var tokens = selector.split(' '); - var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; - var currentContext = [document$1]; - for (i = 0; i < tokens.length; i++) { - token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); - if (token.indexOf('#') > -1) { - // Token is an ID selector - bits = token.split('#'); - tagName = bits[0]; - var id = bits[1]; - var element = document$1.getElementById(id); - if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { - // element not found or tag with that ID not found, return false - return []; - } - // Set currentContext to contain just this element - currentContext = [element]; - continue; // Skip to next token - } - if (token.indexOf('.') > -1) { - // Token contains a class selector - bits = token.split('.'); - tagName = bits[0]; - var className = bits[1]; - if (!tagName) { - tagName = '*'; - } - // Get elements matching tag, filter them for class selector - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - if (tagName == '*') { - elements = getAllChildren(currentContext[j]); - } else { - elements = currentContext[j].getElementsByTagName(tagName); - } - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = []; - currentContextIndex = 0; - for (j = 0; j < found.length; j++) { - if (found[j].className && - _.isString(found[j].className) && // some SVG elements have classNames which are not strings - hasClass(found[j], className) - ) { - currentContext[currentContextIndex++] = found[j]; - } - } - continue; // Skip to next token - } - // Code to deal with attribute selectors - var token_match = token.match(TOKEN_MATCH_REGEX); - if (token_match) { - tagName = token_match[1]; - var attrName = token_match[2]; - var attrOperator = token_match[3]; - var attrValue = token_match[4]; - if (!tagName) { - tagName = '*'; - } - // Grab all of the tagName elements within current context - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - if (tagName == '*') { - elements = getAllChildren(currentContext[j]); - } else { - elements = currentContext[j].getElementsByTagName(tagName); - } - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = []; - currentContextIndex = 0; - var checkFunction; // This function will be used to filter the elements - switch (attrOperator) { - case '=': // Equality - checkFunction = function(e) { - return (e.getAttribute(attrName) == attrValue); - }; - break; - case '~': // Match one of space seperated words - checkFunction = function(e) { - return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); - }; - break; - case '|': // Match start with value followed by optional hyphen - checkFunction = function(e) { - return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); - }; - break; - case '^': // Match starts with value - checkFunction = function(e) { - return (e.getAttribute(attrName).indexOf(attrValue) === 0); - }; - break; - case '$': // Match ends with value - fails with "Warning" in Opera 7 - checkFunction = function(e) { - return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); - }; - break; - case '*': // Match ends with value - checkFunction = function(e) { - return (e.getAttribute(attrName).indexOf(attrValue) > -1); - }; - break; - default: - // Just test for existence of attribute - checkFunction = function(e) { - return e.getAttribute(attrName); - }; - } - currentContext = []; - currentContextIndex = 0; - for (j = 0; j < found.length; j++) { - if (checkFunction(found[j])) { - currentContext[currentContextIndex++] = found[j]; - } - } - // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); - continue; // Skip to next token - } - // If we get here, token is JUST an element (not a class or ID selector) - tagName = token; - found = []; - foundCount = 0; - for (j = 0; j < currentContext.length; j++) { - elements = currentContext[j].getElementsByTagName(tagName); - for (k = 0; k < elements.length; k++) { - found[foundCount++] = elements[k]; - } - } - currentContext = found; - } - return currentContext; - } - - return function(query) { - if (_.isElement(query)) { - return [query]; - } else if (_.isObject(query) && !_.isUndefined(query.length)) { - return query; - } else { - return getElementsBySelector.call(this, query); - } - }; -})(); - -var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; -var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid']; - -_.info = { - campaignParams: function(default_value) { - var kw = '', - params = {}; - _.each(CAMPAIGN_KEYWORDS, function(kwkey) { - kw = _.getQueryParam(document$1.URL, kwkey); - if (kw.length) { - params[kwkey] = kw; - } else if (default_value !== undefined) { - params[kwkey] = default_value; - } - }); - - return params; - }, - - clickParams: function() { - var id = '', - params = {}; - _.each(CLICK_IDS, function(idkey) { - id = _.getQueryParam(document$1.URL, idkey); - if (id.length) { - params[idkey] = id; - } - }); - - return params; - }, - - marketingParams: function() { - return _.extend(_.info.campaignParams(), _.info.clickParams()); - }, - - searchEngine: function(referrer) { - if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { - return 'google'; - } else if (referrer.search('https?://(.*)bing.com') === 0) { - return 'bing'; - } else if (referrer.search('https?://(.*)yahoo.com') === 0) { - return 'yahoo'; - } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { - return 'duckduckgo'; - } else { - return null; - } - }, - - searchInfo: function(referrer) { - var search = _.info.searchEngine(referrer), - param = (search != 'yahoo') ? 'q' : 'p', - ret = {}; - - if (search !== null) { - ret['$search_engine'] = search; - - var keyword = _.getQueryParam(referrer, param); - if (keyword.length) { - ret['mp_keyword'] = keyword; - } - } - - return ret; - }, - - /** - * This function detects which browser is running this script. - * The order of the checks are important since many user agents - * include key words used in later checks. - */ - browser: function(user_agent, vendor, opera) { - vendor = vendor || ''; // vendor is undefined for at least IE9 - if (opera || _.includes(user_agent, ' OPR/')) { - if (_.includes(user_agent, 'Mini')) { - return 'Opera Mini'; - } - return 'Opera'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { - return 'BlackBerry'; - } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { - return 'Internet Explorer Mobile'; - } else if (_.includes(user_agent, 'SamsungBrowser/')) { - // https://developer.samsung.com/internet/user-agent-string-format - return 'Samsung Internet'; - } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { - return 'Microsoft Edge'; - } else if (_.includes(user_agent, 'FBIOS')) { - return 'Facebook Mobile'; - } else if (_.includes(user_agent, 'Chrome')) { - return 'Chrome'; - } else if (_.includes(user_agent, 'CriOS')) { - return 'Chrome iOS'; - } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { - return 'UC Browser'; - } else if (_.includes(user_agent, 'FxiOS')) { - return 'Firefox iOS'; - } else if (_.includes(vendor, 'Apple')) { - if (_.includes(user_agent, 'Mobile')) { - return 'Mobile Safari'; - } - return 'Safari'; - } else if (_.includes(user_agent, 'Android')) { - return 'Android Mobile'; - } else if (_.includes(user_agent, 'Konqueror')) { - return 'Konqueror'; - } else if (_.includes(user_agent, 'Firefox')) { - return 'Firefox'; - } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { - return 'Internet Explorer'; - } else if (_.includes(user_agent, 'Gecko')) { - return 'Mozilla'; - } else { - return ''; - } - }, - - /** - * This function detects which browser version is running this script, - * parsing major and minor version (e.g., 42.1). User agent strings from: - * http://www.useragentstring.com/pages/useragentstring.php - */ - browserVersion: function(userAgent, vendor, opera) { - var browser = _.info.browser(userAgent, vendor, opera); - var versionRegexs = { - 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, - 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, - 'Chrome': /Chrome\/(\d+(\.\d+)?)/, - 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, - 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, - 'Safari': /Version\/(\d+(\.\d+)?)/, - 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, - 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, - 'Firefox': /Firefox\/(\d+(\.\d+)?)/, - 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, - 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, - 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, - 'Android Mobile': /android\s(\d+(\.\d+)?)/, - 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, - 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, - 'Mozilla': /rv:(\d+(\.\d+)?)/ - }; - var regex = versionRegexs[browser]; - if (regex === undefined) { - return null; - } - var matches = userAgent.match(regex); - if (!matches) { - return null; - } - return parseFloat(matches[matches.length - 2]); - }, - - os: function() { - var a = userAgent; - if (/Windows/i.test(a)) { - if (/Phone/.test(a) || /WPDesktop/.test(a)) { - return 'Windows Phone'; - } - return 'Windows'; - } else if (/(iPhone|iPad|iPod)/.test(a)) { - return 'iOS'; - } else if (/Android/.test(a)) { - return 'Android'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { - return 'BlackBerry'; - } else if (/Mac/i.test(a)) { - return 'Mac OS X'; - } else if (/Linux/.test(a)) { - return 'Linux'; - } else if (/CrOS/.test(a)) { - return 'Chrome OS'; - } else { - return ''; - } - }, - - device: function(user_agent) { - if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { - return 'Windows Phone'; - } else if (/iPad/.test(user_agent)) { - return 'iPad'; - } else if (/iPod/.test(user_agent)) { - return 'iPod Touch'; - } else if (/iPhone/.test(user_agent)) { - return 'iPhone'; - } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { - return 'BlackBerry'; - } else if (/Android/.test(user_agent)) { - return 'Android'; - } else { - return ''; - } - }, - - referringDomain: function(referrer) { - var split = referrer.split('/'); - if (split.length >= 3) { - return split[2]; - } - return ''; - }, - - currentUrl: function() { - return win.location.href; - }, - - properties: function(extra_props) { - if (typeof extra_props !== 'object') { - extra_props = {}; - } - return _.extend(_.strip_empty_properties({ - '$os': _.info.os(), - '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera), - '$referrer': document$1.referrer, - '$referring_domain': _.info.referringDomain(document$1.referrer), - '$device': _.info.device(userAgent) - }), { - '$current_url': _.info.currentUrl(), - '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera), - '$screen_height': screen.height, - '$screen_width': screen.width, - 'mp_lib': 'web', - '$lib_version': Config.LIB_VERSION, - '$insert_id': cheap_guid(), - 'time': _.timestamp() / 1000 // epoch time in seconds - }, _.strip_empty_properties(extra_props)); - }, - - people_properties: function() { - return _.extend(_.strip_empty_properties({ - '$os': _.info.os(), - '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera) - }), { - '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera) - }); - }, - - mpPageViewProperties: function() { - return _.strip_empty_properties({ - 'current_page_title': document$1.title, - 'current_domain': win.location.hostname, - 'current_url_path': win.location.pathname, - 'current_url_protocol': win.location.protocol, - 'current_url_search': win.location.search - }); - } -}; - -var cheap_guid = function(maxlen) { - var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); - return maxlen ? guid.substring(0, maxlen) : guid; -}; - -// naive way to extract domain name (example.com) from full hostname (my.sub.example.com) -var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i; -// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk -var DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i; -/** - * Attempts to extract main domain name from full hostname, using a few blunt heuristics. For - * common TLDs like .com/.org that always have a simple SLD.TLD structure (example.com), we - * simply extract the last two .-separated parts of the hostname (SIMPLE_DOMAIN_MATCH_REGEX). - * For others, we attempt to account for short ccSLD+TLD combos (.ac.uk) with the legacy - * DOMAIN_MATCH_REGEX (kept to maintain backwards compatibility with existing Mixpanel - * integrations). The only _reliable_ way to extract domain from hostname is with an up-to-date - * list like at https://publicsuffix.org/ so for cases that this helper fails at, the SDK - * offers the 'cookie_domain' config option to set it explicitly. - * @example - * extract_domain('my.sub.example.com') - * // 'example.com' - */ -var extract_domain = function(hostname) { - var domain_regex = DOMAIN_MATCH_REGEX; - var parts = hostname.split('.'); - var tld = parts[parts.length - 1]; - if (tld.length > 4 || tld === 'com' || tld === 'org') { - domain_regex = SIMPLE_DOMAIN_MATCH_REGEX; - } - var matches = hostname.match(domain_regex); - return matches ? matches[0] : ''; -}; - -var JSONStringify = null, JSONParse = null; -if (typeof JSON !== 'undefined') { - JSONStringify = JSON.stringify; - JSONParse = JSON.parse; -} -JSONStringify = JSONStringify || _.JSONEncode; -JSONParse = JSONParse || _.JSONDecode; - -// EXPORTS (for closure compiler) -_['toArray'] = _.toArray; -_['isObject'] = _.isObject; -_['JSONEncode'] = _.JSONEncode; -_['JSONDecode'] = _.JSONDecode; -_['isBlockedUA'] = _.isBlockedUA; -_['isEmptyObject'] = _.isEmptyObject; -_['info'] = _.info; -_['info']['device'] = _.info.device; -_['info']['browser'] = _.info.browser; -_['info']['browserVersion'] = _.info.browserVersion; -_['info']['properties'] = _.info.properties; - -/* eslint camelcase: "off" */ - -/** - * DomTracker Object - * @constructor - */ -var DomTracker = function() {}; - - -// interface -DomTracker.prototype.create_properties = function() {}; -DomTracker.prototype.event_handler = function() {}; -DomTracker.prototype.after_track_handler = function() {}; - -DomTracker.prototype.init = function(mixpanel_instance) { - this.mp = mixpanel_instance; - return this; -}; - -/** - * @param {Object|string} query - * @param {string} event_name - * @param {Object=} properties - * @param {function=} user_callback - */ -DomTracker.prototype.track = function(query, event_name, properties, user_callback) { - var that = this; - var elements = _.dom_query(query); - - if (elements.length === 0) { - console.error('The DOM query (' + query + ') returned 0 elements'); - return; - } - - _.each(elements, function(element) { - _.register_event(element, this.override_event, function(e) { - var options = {}; - var props = that.create_properties(properties, this); - var timeout = that.mp.get_config('track_links_timeout'); - - that.event_handler(e, this, options); - - // in case the mixpanel servers don't get back to us in time - window.setTimeout(that.track_callback(user_callback, props, options, true), timeout); - - // fire the tracking event - that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); - }); - }, this); - - return true; -}; - -/** - * @param {function} user_callback - * @param {Object} props - * @param {boolean=} timeout_occured - */ -DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { - timeout_occured = timeout_occured || false; - var that = this; - - return function() { - // options is referenced from both callbacks, so we can have - // a 'lock' of sorts to ensure only one fires - if (options.callback_fired) { return; } - options.callback_fired = true; - - if (user_callback && user_callback(timeout_occured, props) === false) { - // user can prevent the default functionality by - // returning false from their callback - return; - } - - that.after_track_handler(props, options, timeout_occured); - }; -}; - -DomTracker.prototype.create_properties = function(properties, element) { - var props; - - if (typeof(properties) === 'function') { - props = properties(element); - } else { - props = _.extend({}, properties); - } - - return props; -}; - -/** - * LinkTracker Object - * @constructor - * @extends DomTracker - */ -var LinkTracker = function() { - this.override_event = 'click'; -}; -_.inherit(LinkTracker, DomTracker); - -LinkTracker.prototype.create_properties = function(properties, element) { - var props = LinkTracker.superclass.create_properties.apply(this, arguments); - - if (element.href) { props['url'] = element.href; } - - return props; -}; - -LinkTracker.prototype.event_handler = function(evt, element, options) { - options.new_tab = ( - evt.which === 2 || - evt.metaKey || - evt.ctrlKey || - element.target === '_blank' - ); - options.href = element.href; - - if (!options.new_tab) { - evt.preventDefault(); - } -}; - -LinkTracker.prototype.after_track_handler = function(props, options) { - if (options.new_tab) { return; } - - setTimeout(function() { - window.location = options.href; - }, 0); -}; - -/** - * FormTracker Object - * @constructor - * @extends DomTracker - */ -var FormTracker = function() { - this.override_event = 'submit'; -}; -_.inherit(FormTracker, DomTracker); - -FormTracker.prototype.event_handler = function(evt, element, options) { - options.element = element; - evt.preventDefault(); -}; - -FormTracker.prototype.after_track_handler = function(props, options) { - setTimeout(function() { - options.element.submit(); - }, 0); -}; - -var logger$2 = console_with_prefix('lock'); - -/** - * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser - * window/tab at a time will be able to access shared resources. - * - * Based on the Alur and Taubenfeld fast lock - * (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html) - * with an added timeout to ensure there will be eventual progress in the event - * that a window is closed in the middle of the callback. - * - * Implementation based on the original version by David Wolever (https://github.com/wolever) - * at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432. - * - * @example - * const myLock = new SharedLock('some-key'); - * myLock.withLock(function() { - * console.log('I hold the mutex!'); - * }); - * - * @constructor - */ -var SharedLock = function(key, options) { - options = options || {}; - - this.storageKey = key; - this.storage = options.storage || window.localStorage; - this.pollIntervalMS = options.pollIntervalMS || 100; - this.timeoutMS = options.timeoutMS || 2000; -}; - -// pass in a specific pid to test contention scenarios; otherwise -// it is chosen randomly for each acquisition attempt -SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) { - if (!pid && typeof errorCB !== 'function') { - pid = errorCB; - errorCB = null; - } - - var i = pid || (new Date().getTime() + '|' + Math.random()); - var startTime = new Date().getTime(); - - var key = this.storageKey; - var pollIntervalMS = this.pollIntervalMS; - var timeoutMS = this.timeoutMS; - var storage = this.storage; - - var keyX = key + ':X'; - var keyY = key + ':Y'; - var keyZ = key + ':Z'; - - var reportError = function(err) { - errorCB && errorCB(err); - }; - - var delay = function(cb) { - if (new Date().getTime() - startTime > timeoutMS) { - logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']'); - storage.removeItem(keyZ); - storage.removeItem(keyY); - loop(); - return; - } - setTimeout(function() { - try { - cb(); - } catch(err) { - reportError(err); - } - }, pollIntervalMS * (Math.random() + 0.1)); - }; - - var waitFor = function(predicate, cb) { - if (predicate()) { - cb(); - } else { - delay(function() { - waitFor(predicate, cb); - }); - } - }; - - var getSetY = function() { - var valY = storage.getItem(keyY); - if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases) - return false; - } else { - storage.setItem(keyY, i); - if (storage.getItem(keyY) === i) { - return true; - } else { - if (!localStorageSupported(storage, true)) { - throw new Error('localStorage support dropped while acquiring lock'); - } - return false; - } - } - }; - - var loop = function() { - storage.setItem(keyX, i); - - waitFor(getSetY, function() { - if (storage.getItem(keyX) === i) { - criticalSection(); - return; - } - - delay(function() { - if (storage.getItem(keyY) !== i) { - loop(); - return; - } - waitFor(function() { - return !storage.getItem(keyZ); - }, criticalSection); - }); - }); - }; - - var criticalSection = function() { - storage.setItem(keyZ, '1'); - try { - lockedCB(); - } finally { - storage.removeItem(keyZ); - if (storage.getItem(keyY) === i) { - storage.removeItem(keyY); - } - if (storage.getItem(keyX) === i) { - storage.removeItem(keyX); - } - } - }; - - try { - if (localStorageSupported(storage, true)) { - loop(); - } else { - throw new Error('localStorage support check failed'); - } - } catch(err) { - reportError(err); - } -}; - -var logger$1 = console_with_prefix('batch'); - -/** - * RequestQueue: queue for batching API requests with localStorage backup for retries. - * Maintains an in-memory queue which represents the source of truth for the current - * page, but also writes all items out to a copy in the browser's localStorage, which - * can be read on subsequent pageloads and retried. For batchability, all the request - * items in the queue should be of the same type (events, people updates, group updates) - * so they can be sent in a single request to the same API endpoint. - * - * LocalStorage keying and locking: In order for reloads and subsequent pageloads of - * the same site to access the same persisted data, they must share the same localStorage - * key (for instance based on project token and queue type). Therefore access to the - * localStorage entry is guarded by an asynchronous mutex (SharedLock) to prevent - * simultaneously open windows/tabs from overwriting each other's data (which would lead - * to data loss in some situations). - * @constructor - */ -var RequestQueue = function(storageKey, options) { - options = options || {}; - this.storageKey = storageKey; - this.storage = options.storage || window.localStorage; - this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1); - this.lock = new SharedLock(storageKey, {storage: this.storage}); - - this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios - - this.memQueue = []; -}; - -/** - * Add one item to queues (memory and localStorage). The queued entry includes - * the given item along with an auto-generated ID and a "flush-after" timestamp. - * It is expected that the item will be sent over the network and dequeued - * before the flush-after time; if this doesn't happen it is considered orphaned - * (e.g., the original tab where it was enqueued got closed before it could be - * sent) and the item can be sent by any tab that finds it in localStorage. - * - * The final callback param is called with a param indicating success or - * failure of the enqueue operation; it is asynchronous because the localStorage - * lock is asynchronous. - */ -RequestQueue.prototype.enqueue = function(item, flushInterval, cb) { - var queueEntry = { - 'id': cheap_guid(), - 'flushAfter': new Date().getTime() + flushInterval * 2, - 'payload': item - }; - - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue.push(queueEntry); - succeeded = this.saveToStorage(storedQueue); - if (succeeded) { - // only add to in-memory queue when storage succeeds - this.memQueue.push(queueEntry); - } - } catch(err) { - this.reportError('Error enqueueing item', item); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); -}; - -/** - * Read out the given number of queue entries. If this.memQueue - * has fewer than batchSize items, then look for "orphaned" items - * in the persisted queue (items where the 'flushAfter' time has - * already passed). - */ -RequestQueue.prototype.fillBatch = function(batchSize) { - var batch = this.memQueue.slice(0, batchSize); - if (batch.length < batchSize) { - // don't need lock just to read events; localStorage is thread-safe - // and the worst that could happen is a duplicate send of some - // orphaned events, which will be deduplicated on the server side - var storedQueue = this.readFromStorage(); - if (storedQueue.length) { - // item IDs already in batch; don't duplicate out of storage - var idsInBatch = {}; // poor man's Set - _.each(batch, function(item) { idsInBatch[item['id']] = true; }); - - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) { - item.orphaned = true; - batch.push(item); - if (batch.length >= batchSize) { - break; - } - } - } - } - } - return batch; -}; - -/** - * Remove items with matching 'id' from array (immutably) - * also remove any item without a valid id (e.g., malformed - * storage entries). - */ -var filterOutIDsAndInvalid = function(items, idSet) { - var filteredItems = []; - _.each(items, function(item) { - if (item['id'] && !idSet[item['id']]) { - filteredItems.push(item); - } - }); - return filteredItems; -}; - -/** - * Remove items with matching IDs from both in-memory queue - * and persisted queue - */ -RequestQueue.prototype.removeItemsByID = function(ids, cb) { - var idSet = {}; // poor man's Set - _.each(ids, function(id) { idSet[id] = true; }); - - this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet); - - var removeFromStorage = _.bind(function() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = filterOutIDsAndInvalid(storedQueue, idSet); - succeeded = this.saveToStorage(storedQueue); - - // an extra check: did storage report success but somehow - // the items are still there? - if (succeeded) { - storedQueue = this.readFromStorage(); - for (var i = 0; i < storedQueue.length; i++) { - var item = storedQueue[i]; - if (item['id'] && !!idSet[item['id']]) { - this.reportError('Item not removed from storage'); - return false; - } - } - } - } catch(err) { - this.reportError('Error removing items', ids); - succeeded = false; - } - return succeeded; - }, this); - - this.lock.withLock(function lockAcquired() { - var succeeded = removeFromStorage(); - if (cb) { - cb(succeeded); - } - }, _.bind(function lockFailure(err) { - var succeeded = false; - this.reportError('Error acquiring storage lock', err); - if (!localStorageSupported(this.storage, true)) { - // Looks like localStorage writes have stopped working sometime after - // initialization (probably full), and so nobody can acquire locks - // anymore. Consider it temporarily safe to remove items without the - // lock, since nobody's writing successfully anyway. - succeeded = removeFromStorage(); - if (!succeeded) { - // OK, we couldn't even write out the smaller queue. Try clearing it - // entirely. - try { - this.storage.removeItem(this.storageKey); - } catch(err) { - this.reportError('Error clearing queue', err); - } - } - } - if (cb) { - cb(succeeded); - } - }, this), this.pid); -}; - -// internal helper for RequestQueue.updatePayloads -var updatePayloads = function(existingItems, itemsToUpdate) { - var newItems = []; - _.each(existingItems, function(item) { - var id = item['id']; - if (id in itemsToUpdate) { - var newPayload = itemsToUpdate[id]; - if (newPayload !== null) { - item['payload'] = newPayload; - newItems.push(item); - } - } else { - // no update - newItems.push(item); - } - }); - return newItems; -}; - -/** - * Update payloads of given items in both in-memory queue and - * persisted queue. Items set to null are removed from queues. - */ -RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) { - this.memQueue = updatePayloads(this.memQueue, itemsToUpdate); - this.lock.withLock(_.bind(function lockAcquired() { - var succeeded; - try { - var storedQueue = this.readFromStorage(); - storedQueue = updatePayloads(storedQueue, itemsToUpdate); - succeeded = this.saveToStorage(storedQueue); - } catch(err) { - this.reportError('Error updating items', itemsToUpdate); - succeeded = false; - } - if (cb) { - cb(succeeded); - } - }, this), _.bind(function lockFailure(err) { - this.reportError('Error acquiring storage lock', err); - if (cb) { - cb(false); - } - }, this), this.pid); -}; - -/** - * Read and parse items array from localStorage entry, handling - * malformed/missing data if necessary. - */ -RequestQueue.prototype.readFromStorage = function() { - var storageEntry; - try { - storageEntry = this.storage.getItem(this.storageKey); - if (storageEntry) { - storageEntry = JSONParse(storageEntry); - if (!_.isArray(storageEntry)) { - this.reportError('Invalid storage entry:', storageEntry); - storageEntry = null; - } - } - } catch (err) { - this.reportError('Error retrieving queue', err); - storageEntry = null; - } - return storageEntry || []; -}; - -/** - * Serialize the given items array to localStorage. - */ -RequestQueue.prototype.saveToStorage = function(queue) { - try { - this.storage.setItem(this.storageKey, JSONStringify(queue)); - return true; - } catch (err) { - this.reportError('Error saving queue', err); - return false; - } -}; - -/** - * Clear out queues (memory and localStorage). - */ -RequestQueue.prototype.clear = function() { - this.memQueue = []; - this.storage.removeItem(this.storageKey); -}; - -// maximum interval between request retries after exponential backoff -var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes - -var logger = console_with_prefix('batch'); - -/** - * RequestBatcher: manages the queueing, flushing, retry etc of requests of one - * type (events, people, groups). - * Uses RequestQueue to manage the backing store. - * @constructor - */ -var RequestBatcher = function(storageKey, options) { - this.errorReporter = options.errorReporter; - this.queue = new RequestQueue(storageKey, { - errorReporter: _.bind(this.reportError, this), - storage: options.storage - }); - - this.libConfig = options.libConfig; - this.sendRequest = options.sendRequestFunc; - this.beforeSendHook = options.beforeSendHook; - this.stopAllBatching = options.stopAllBatchingFunc; - - // seed variable batch size + flush interval with configured values - this.batchSize = this.libConfig['batch_size']; - this.flushInterval = this.libConfig['batch_flush_interval_ms']; - - this.stopped = !this.libConfig['batch_autostart']; - this.consecutiveRemovalFailures = 0; - - // extra client-side dedupe - this.itemIdsSentSuccessfully = {}; -}; - -/** - * Add one item to queue. - */ -RequestBatcher.prototype.enqueue = function(item, cb) { - this.queue.enqueue(item, this.flushInterval, cb); -}; - -/** - * Start flushing batches at the configured time interval. Must call - * this method upon SDK init in order to send anything over the network. - */ -RequestBatcher.prototype.start = function() { - this.stopped = false; - this.consecutiveRemovalFailures = 0; - this.flush(); -}; - -/** - * Stop flushing batches. Can be restarted by calling start(). - */ -RequestBatcher.prototype.stop = function() { - this.stopped = true; - if (this.timeoutID) { - clearTimeout(this.timeoutID); - this.timeoutID = null; - } -}; - -/** - * Clear out queue. - */ -RequestBatcher.prototype.clear = function() { - this.queue.clear(); -}; - -/** - * Restore batch size configuration to whatever is set in the main SDK. - */ -RequestBatcher.prototype.resetBatchSize = function() { - this.batchSize = this.libConfig['batch_size']; -}; - -/** - * Restore flush interval time configuration to whatever is set in the main SDK. - */ -RequestBatcher.prototype.resetFlush = function() { - this.scheduleFlush(this.libConfig['batch_flush_interval_ms']); -}; - -/** - * Schedule the next flush in the given number of milliseconds. - */ -RequestBatcher.prototype.scheduleFlush = function(flushMS) { - this.flushInterval = flushMS; - if (!this.stopped) { // don't schedule anymore if batching has been stopped - this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval); - } -}; - -/** - * Flush one batch to network. Depending on success/failure modes, it will either - * remove the batch from the queue or leave it in for retry, and schedule the next - * flush. In cases of most network or API failures, it will back off exponentially - * when retrying. - * @param {Object} [options] - * @param {boolean} [options.sendBeacon] - whether to send batch with - * navigator.sendBeacon (only useful for sending batches before page unloads, as - * sendBeacon offers no callbacks or status indications) - */ -RequestBatcher.prototype.flush = function(options) { - try { - - if (this.requestInProgress) { - logger.log('Flush: Request already in progress'); - return; - } - - options = options || {}; - var timeoutMS = this.libConfig['batch_request_timeout_ms']; - var startTime = new Date().getTime(); - var currentBatchSize = this.batchSize; - var batch = this.queue.fillBatch(currentBatchSize); - var dataForRequest = []; - var transformedItems = {}; - _.each(batch, function(item) { - var payload = item['payload']; - if (this.beforeSendHook && !item.orphaned) { - payload = this.beforeSendHook(payload); - } - if (payload) { - // mp_sent_by_lib_version prop captures which lib version actually - // sends each event (regardless of which version originally queued - // it for sending) - if (payload['event'] && payload['properties']) { - payload['properties'] = _.extend( - {}, - payload['properties'], - {'mp_sent_by_lib_version': Config.LIB_VERSION} - ); - } - var addPayload = true; - var itemId = item['id']; - if (itemId) { - if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) { - this.reportError('[dupe] item ID sent too many times, not sending', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - addPayload = false; - } - } else { - this.reportError('[dupe] found item with no ID', {item: item}); - } - - if (addPayload) { - dataForRequest.push(payload); - } - } - transformedItems[item['id']] = payload; - }, this); - if (dataForRequest.length < 1) { - this.resetFlush(); - return; // nothing to do - } - - this.requestInProgress = true; - - var batchSendCallback = _.bind(function(res) { - this.requestInProgress = false; - - try { - - // handle API response in a try-catch to make sure we can reset the - // flush operation if something goes wrong - - var removeItemsFromQueue = false; - if (options.unloading) { - // update persisted data to include hook transformations - this.queue.updatePayloads(transformedItems); - } else if ( - _.isObject(res) && - res.error === 'timeout' && - new Date().getTime() - startTime >= timeoutMS - ) { - this.reportError('Network timeout; retrying'); - this.flush(); - } else if ( - _.isObject(res) && - res.xhr_req && - (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout') - ) { - // network or API error, or 429 Too Many Requests, retry - var retryMS = this.flushInterval * 2; - var headers = res.xhr_req['responseHeaders']; - if (headers) { - var retryAfter = headers['Retry-After']; - if (retryAfter) { - retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS; - } - } - retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS); - this.reportError('Error; retry in ' + retryMS + ' ms'); - this.scheduleFlush(retryMS); - } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) { - // 413 Payload Too Large - if (batch.length > 1) { - var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2)); - this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1); - this.reportError('413 response; reducing batch size to ' + this.batchSize); - this.resetFlush(); - } else { - this.reportError('Single-event request too large; dropping', batch); - this.resetBatchSize(); - removeItemsFromQueue = true; - } - } else { - // successful network request+response; remove each item in batch from queue - // (even if it was e.g. a 400, in which case retrying won't help) - removeItemsFromQueue = true; - } - - if (removeItemsFromQueue) { - this.queue.removeItemsByID( - _.map(batch, function(item) { return item['id']; }), - _.bind(function(succeeded) { - if (succeeded) { - this.consecutiveRemovalFailures = 0; - this.flush(); // handle next batch if the queue isn't empty - } else { - this.reportError('Failed to remove items from queue'); - if (++this.consecutiveRemovalFailures > 5) { - this.reportError('Too many queue failures; disabling batching system.'); - this.stopAllBatching(); - } else { - this.resetFlush(); - } - } - }, this) - ); - - // client-side dedupe - _.each(batch, _.bind(function(item) { - var itemId = item['id']; - if (itemId) { - this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0; - this.itemIdsSentSuccessfully[itemId]++; - if (this.itemIdsSentSuccessfully[itemId] > 5) { - this.reportError('[dupe] item ID sent too many times', { - item: item, - batchSize: batch.length, - timesSent: this.itemIdsSentSuccessfully[itemId] - }); - } - } else { - this.reportError('[dupe] found item with no ID while removing', {item: item}); - } - }, this)); - } - - } catch(err) { - this.reportError('Error handling API response', err); - this.resetFlush(); - } - }, this); - var requestOptions = { - method: 'POST', - verbose: true, - ignore_json_errors: true, // eslint-disable-line camelcase - timeout_ms: timeoutMS // eslint-disable-line camelcase - }; - if (options.unloading) { - requestOptions.transport = 'sendBeacon'; - } - logger.log('MIXPANEL REQUEST:', dataForRequest); - this.sendRequest(dataForRequest, requestOptions, batchSendCallback); - - } catch(err) { - this.reportError('Error flushing request queue', err); - this.resetFlush(); - } -}; - -/** - * Log error to global logger and optional user-defined logger. - */ -RequestBatcher.prototype.reportError = function(msg, err) { - logger.error.apply(logger.error, arguments); - if (this.errorReporter) { - try { - if (!(err instanceof Error)) { - err = new Error(msg); - } - this.errorReporter(msg, err); - } catch(err) { - logger.error(err); - } - } -}; - -/** - * GDPR utils - * - * The General Data Protection Regulation (GDPR) is a regulation in EU law on data protection - * and privacy for all individuals within the European Union. It addresses the export of personal - * data outside the EU. The GDPR aims primarily to give control back to citizens and residents - * over their personal data and to simplify the regulatory environment for international business - * by unifying the regulation within the EU. - * - * This set of utilities is intended to enable opt in/out functionality in the Mixpanel JS SDK. - * These functions are used internally by the SDK and are not intended to be publicly exposed. - */ - -/** - * A function used to track a Mixpanel event (e.g. MixpanelLib.track) - * @callback trackFunction - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - */ - -/** Public **/ - -var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; - -/** - * Opt the user in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function optIn(token, options) { - _optInOut(true, token, options); -} - -/** - * Opt the user out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not - */ -function optOut(token, options) { - _optInOut(false, token, options); -} - -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {boolean} whether the user has opted in to the given opt type - */ -function hasOptedIn(token, options) { - return _getStorageValue(token, options) === '1'; -} - -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the user has opted out of the given opt type - */ -function hasOptedOut(token, options) { - if (_hasDoNotTrackFlagOn(options)) { - console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"'); - return true; - } - var optedOut = _getStorageValue(token, options) === '0'; - if (optedOut) { - console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.'); - } - return optedOut; -} - -/** - * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelLib(method) { - return _addOptOutCheck(method, function(name) { - return this.get_config(name); - }); -} - -/** - * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelPeople(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); - }); -} - -/** - * Wrap a MixpanelGroup method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function addOptOutCheckMixpanelGroup(method) { - return _addOptOutCheck(method, function(name) { - return this._get_config(name); - }); -} - -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function clearOptInOut(token, options) { - options = options || {}; - _getStorage(options).remove( - _getStorageKey(token, options), !!options.crossSubdomainCookie, options.cookieDomain - ); -} - -/** Private **/ - -/** - * Get storage util - * @param {Object} [options] - * @param {string} [options.persistenceType] - * @returns {object} either _.cookie or _.localstorage - */ -function _getStorage(options) { - options = options || {}; - return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; -} - -/** - * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the name of the cookie for the given opt type - */ -function _getStorageKey(token, options) { - options = options || {}; - return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; -} - -/** - * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @returns {string} the value of the cookie for the given opt type - */ -function _getStorageValue(token, options) { - return _getStorage(options).get(_getStorageKey(token, options)); -} - -/** - * Check whether the user has set the DNT/doNotTrack setting to true in their browser - * @param {Object} [options] - * @param {string} [options.window] - alternate window object to check; used to force various DNT settings in browser tests - * @param {boolean} [options.ignoreDnt] - flag to ignore browser DNT settings and always return false - * @returns {boolean} whether the DNT setting is true - */ -function _hasDoNotTrackFlagOn(options) { - if (options && options.ignoreDnt) { - return false; - } - var win$1 = (options && options.window) || win; - var nav = win$1['navigator'] || {}; - var hasDntOn = false; - - _.each([ - nav['doNotTrack'], // standard - nav['msDoNotTrack'], - win$1['doNotTrack'] - ], function(dntValue) { - if (_.includes([true, 1, '1', 'yes'], dntValue)) { - hasDntOn = true; - } - }); - - return hasDntOn; -} - -/** - * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type - * @param {boolean} optValue - whether to opt the user in or out for the given opt type - * @param {string} token - Mixpanel project tracking token - * @param {Object} [options] - * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action - * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action - * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action - * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires - * @param {string} [options.cookieDomain] - custom cookie domain - * @param {boolean} [options.crossSiteCookie] - whether the opt-in cookie is set as cross-site-enabled - * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not - * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not - */ -function _optInOut(optValue, token, options) { - if (!_.isString(token) || !token.length) { - console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); - return; - } - - options = options || {}; - - _getStorage(options).set( - _getStorageKey(token, options), - optValue ? 1 : 0, - _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, - !!options.crossSubdomainCookie, - !!options.secureCookie, - !!options.crossSiteCookie, - options.cookieDomain - ); - - if (options.track && optValue) { // only track event if opting in (optValue=true) - options.track(options.trackEventName || '$opt_in', options.trackProperties, { - 'send_immediately': true - }); - } -} - -/** - * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token - * If the user has opted out, return early instead of executing the method. - * If a callback argument was provided, execute it passing the 0 error code. - * @param {function} method - wrapped method to be executed if the user has not opted out - * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check - * @returns {*} the result of executing method OR undefined if the user has opted out - */ -function _addOptOutCheck(method, getConfigValue) { - return function() { - var optedOut = false; - - try { - var token = getConfigValue.call(this, 'token'); - var ignoreDnt = getConfigValue.call(this, 'ignore_dnt'); - var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); - var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); - var win = getConfigValue.call(this, 'window'); // used to override window during browser tests - - if (token) { // if there was an issue getting the token, continue method execution as normal - optedOut = hasOptedOut(token, { - ignoreDnt: ignoreDnt, - persistenceType: persistenceType, - persistencePrefix: persistencePrefix, - window: win - }); - } - } catch(err) { - console.error('Unexpected error when checking tracking opt-out status: ' + err); - } - - if (!optedOut) { - return method.apply(this, arguments); - } - - var callback = arguments[arguments.length - 1]; - if (typeof(callback) === 'function') { - callback(0); - } - - return; - }; -} - -/* eslint camelcase: "off" */ - -/** @const */ var SET_ACTION = '$set'; -/** @const */ var SET_ONCE_ACTION = '$set_once'; -/** @const */ var UNSET_ACTION = '$unset'; -/** @const */ var ADD_ACTION = '$add'; -/** @const */ var APPEND_ACTION = '$append'; -/** @const */ var UNION_ACTION = '$union'; -/** @const */ var REMOVE_ACTION = '$remove'; -/** @const */ var DELETE_ACTION = '$delete'; - -// Common internal methods for mixpanel.people and mixpanel.group APIs. -// These methods shouldn't involve network I/O. -var apiActions = { - set_action: function(prop, to) { - var data = {}; - var $set = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - $set[k] = v; - } - }, this); - } else { - $set[prop] = to; - } - - data[SET_ACTION] = $set; - return data; - }, - - unset_action: function(prop) { - var data = {}; - var $unset = []; - if (!_.isArray(prop)) { - prop = [prop]; - } - - _.each(prop, function(k) { - if (!this._is_reserved_property(k)) { - $unset.push(k); - } - }, this); - - data[UNSET_ACTION] = $unset; - return data; - }, - - set_once_action: function(prop, to) { - var data = {}; - var $set_once = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - $set_once[k] = v; - } - }, this); - } else { - $set_once[prop] = to; - } - data[SET_ONCE_ACTION] = $set_once; - return data; - }, - - union_action: function(list_name, values) { - var data = {}; - var $union = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $union[k] = _.isArray(v) ? v : [v]; - } - }, this); - } else { - $union[list_name] = _.isArray(values) ? values : [values]; - } - data[UNION_ACTION] = $union; - return data; - }, - - append_action: function(list_name, value) { - var data = {}; - var $append = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $append[k] = v; - } - }, this); - } else { - $append[list_name] = value; - } - data[APPEND_ACTION] = $append; - return data; - }, - - remove_action: function(list_name, value) { - var data = {}; - var $remove = {}; - if (_.isObject(list_name)) { - _.each(list_name, function(v, k) { - if (!this._is_reserved_property(k)) { - $remove[k] = v; - } - }, this); - } else { - $remove[list_name] = value; - } - data[REMOVE_ACTION] = $remove; - return data; - }, - - delete_action: function() { - var data = {}; - data[DELETE_ACTION] = ''; - return data; - } -}; - -/* eslint camelcase: "off" */ - -/** - * Mixpanel Group Object - * @constructor - */ -var MixpanelGroup = function() {}; - -_.extend(MixpanelGroup.prototype, apiActions); - -MixpanelGroup.prototype._init = function(mixpanel_instance, group_key, group_id) { - this._mixpanel = mixpanel_instance; - this._group_key = group_key; - this._group_id = group_id; -}; - -/** - * Set properties on a group. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').set('Location', '405 Howard'); - * - * // or set multiple properties at once - * mixpanel.get_group('company', 'mixpanel').set({ - * 'Location': '405 Howard', - * 'Founded' : 2009, - * }); - * // properties can be strings, integers, dates, or lists - * - * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. - * @param {*} [to] A value to set on the given property name - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.set = addOptOutCheckMixpanelGroup(function(prop, to, callback) { - var data = this.set_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/** - * Set properties on a group, only if they do not yet exist. - * This will not overwrite previous group property values, unlike - * group.set(). - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').set_once('Location', '405 Howard'); - * - * // or set multiple properties at once - * mixpanel.get_group('company', 'mixpanel').set_once({ - * 'Location': '405 Howard', - * 'Founded' : 2009, - * }); - * // properties can be strings, integers, lists or dates - * - * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. - * @param {*} [to] A value to set on the given property name - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.set_once = addOptOutCheckMixpanelGroup(function(prop, to, callback) { - var data = this.set_once_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/** - * Unset properties on a group permanently. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').unset('Founded'); - * - * @param {String} prop The name of the property. - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.unset = addOptOutCheckMixpanelGroup(function(prop, callback) { - var data = this.unset_action(prop); - return this._send_request(data, callback); -}); - -/** - * Merge a given list with a list-valued group property, excluding duplicate values. - * - * ### Usage: - * - * // merge a value to a list, creating it if needed - * mixpanel.get_group('company', 'mixpanel').union('Location', ['San Francisco', 'London']); - * - * @param {String} list_name Name of the property. - * @param {Array} values Values to merge with the given property - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name, values, callback) { - if (_.isObject(list_name)) { - callback = values; - } - var data = this.union_action(list_name, values); - return this._send_request(data, callback); -}); - -/** - * Permanently delete a group. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').delete(); - * - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) { - // bracket notation above prevents a minification error related to reserved words - var data = this.delete_action(); - return this._send_request(data, callback); -}); - -/** - * Remove a property from a group. The value will be ignored if doesn't exist. - * - * ### Usage: - * - * mixpanel.get_group('company', 'mixpanel').remove('Location', 'London'); - * - * @param {String} list_name Name of the property. - * @param {Object} value Value to remove from the given group property - * @param {Function} [callback] If provided, the callback will be called after the tracking event - */ -MixpanelGroup.prototype.remove = addOptOutCheckMixpanelGroup(function(list_name, value, callback) { - var data = this.remove_action(list_name, value); - return this._send_request(data, callback); -}); - -MixpanelGroup.prototype._send_request = function(data, callback) { - data['$group_key'] = this._group_key; - data['$group_id'] = this._group_id; - data['$token'] = this._get_config('token'); - - var date_encoded_data = _.encodeDates(data); - return this._mixpanel._track_or_batch({ - type: 'groups', - data: date_encoded_data, - endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['groups'], - batcher: this._mixpanel.request_batchers.groups - }, callback); -}; - -MixpanelGroup.prototype._is_reserved_property = function(prop) { - return prop === '$group_key' || prop === '$group_id'; -}; - -MixpanelGroup.prototype._get_config = function(conf) { - return this._mixpanel.get_config(conf); -}; - -MixpanelGroup.prototype.toString = function() { - return this._mixpanel.toString() + '.group.' + this._group_key + '.' + this._group_id; -}; - -// MixpanelGroup Exports -MixpanelGroup.prototype['remove'] = MixpanelGroup.prototype.remove; -MixpanelGroup.prototype['set'] = MixpanelGroup.prototype.set; -MixpanelGroup.prototype['set_once'] = MixpanelGroup.prototype.set_once; -MixpanelGroup.prototype['union'] = MixpanelGroup.prototype.union; -MixpanelGroup.prototype['unset'] = MixpanelGroup.prototype.unset; -MixpanelGroup.prototype['toString'] = MixpanelGroup.prototype.toString; - -/* eslint camelcase: "off" */ - -/** - * Mixpanel People Object - * @constructor - */ -var MixpanelPeople = function() {}; - -_.extend(MixpanelPeople.prototype, apiActions); - -MixpanelPeople.prototype._init = function(mixpanel_instance) { - this._mixpanel = mixpanel_instance; -}; - -/* -* Set properties on a user record. -* -* ### Usage: -* -* mixpanel.people.set('gender', 'm'); -* -* // or set multiple properties at once -* mixpanel.people.set({ -* 'Company': 'Acme', -* 'Plan': 'Premium', -* 'Upgrade date': new Date() -* }); -* // properties can be strings, integers, dates, or lists -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [to] A value to set on the given property name -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { - var data = this.set_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - // make sure that the referrer info has been updated and saved - if (this._get_config('save_referrer')) { - this._mixpanel['persistence'].update_referrer_info(document.referrer); - } - - // update $set object with default people properties - data[SET_ACTION] = _.extend( - {}, - _.info.people_properties(), - data[SET_ACTION] - ); - return this._send_request(data, callback); -}); - -/* -* Set properties on a user record, only if they do not yet exist. -* This will not overwrite previous people property values, unlike -* people.set(). -* -* ### Usage: -* -* mixpanel.people.set_once('First Login Date', new Date()); -* -* // or set multiple properties at once -* mixpanel.people.set_once({ -* 'First Login Date': new Date(), -* 'Starting Plan': 'Premium' -* }); -* -* // properties can be strings, integers or dates -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [to] A value to set on the given property name -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { - var data = this.set_once_action(prop, to); - if (_.isObject(prop)) { - callback = to; - } - return this._send_request(data, callback); -}); - -/* -* Unset properties on a user record (permanently removes the properties and their values from a profile). -* -* ### Usage: -* -* mixpanel.people.unset('gender'); -* -* // or unset multiple properties at once -* mixpanel.people.unset(['gender', 'Company']); -* -* @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.unset = addOptOutCheckMixpanelPeople(function(prop, callback) { - var data = this.unset_action(prop); - return this._send_request(data, callback); -}); - -/* -* Increment/decrement numeric people analytics properties. -* -* ### Usage: -* -* mixpanel.people.increment('page_views', 1); -* -* // or, for convenience, if you're just incrementing a counter by -* // 1, you can simply do -* mixpanel.people.increment('page_views'); -* -* // to decrement a counter, pass a negative number -* mixpanel.people.increment('credits_left', -1); -* -* // like mixpanel.people.set(), you can increment multiple -* // properties at once: -* mixpanel.people.increment({ -* counter1: 1, -* counter2: 6 -* }); -* -* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. -* @param {Number} [by] An amount to increment the given property -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { - var data = {}; - var $add = {}; - if (_.isObject(prop)) { - _.each(prop, function(v, k) { - if (!this._is_reserved_property(k)) { - if (isNaN(parseFloat(v))) { - console.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); - return; - } else { - $add[k] = v; - } - } - }, this); - callback = by; - } else { - // convenience: mixpanel.people.increment('property'); will - // increment 'property' by 1 - if (_.isUndefined(by)) { - by = 1; - } - $add[prop] = by; - } - data[ADD_ACTION] = $add; - - return this._send_request(data, callback); -}); - -/* -* Append a value to a list-valued people analytics property. -* -* ### Usage: -* -* // append a value to a list, creating it if needed -* mixpanel.people.append('pages_visited', 'homepage'); -* -* // like mixpanel.people.set(), you can append multiple -* // properties at once: -* mixpanel.people.append({ -* list1: 'bob', -* list2: 123 -* }); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] value An item to append to the list -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { - if (_.isObject(list_name)) { - callback = value; - } - var data = this.append_action(list_name, value); - return this._send_request(data, callback); -}); - -/* -* Remove a value from a list-valued people analytics property. -* -* ### Usage: -* -* mixpanel.people.remove('School', 'UCB'); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] value Item to remove from the list -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.remove = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { - if (_.isObject(list_name)) { - callback = value; - } - var data = this.remove_action(list_name, value); - return this._send_request(data, callback); -}); - -/* -* Merge a given list with a list-valued people analytics property, -* excluding duplicate values. -* -* ### Usage: -* -* // merge a value to a list, creating it if needed -* mixpanel.people.union('pages_visited', 'homepage'); -* -* // like mixpanel.people.set(), you can append multiple -* // properties at once: -* mixpanel.people.union({ -* list1: 'bob', -* list2: 123 -* }); -* -* // like mixpanel.people.append(), you can append multiple -* // values to the same list: -* mixpanel.people.union({ -* list1: ['bob', 'billy'] -* }); -* -* @param {Object|String} list_name If a string, this is the name of the property. If an object, this is an associative array of names and values. -* @param {*} [value] Value / values to merge with the given property -* @param {Function} [callback] If provided, the callback will be called after tracking the event. -*/ -MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { - if (_.isObject(list_name)) { - callback = values; - } - var data = this.union_action(list_name, values); - return this._send_request(data, callback); -}); - -/* - * Record that you have charged the current user a certain amount - * of money. Charges recorded with track_charge() will appear in the - * Mixpanel revenue report. - * - * ### Usage: - * - * // charge a user $50 - * mixpanel.people.track_charge(50); - * - * // charge a user $30.50 on the 2nd of january - * mixpanel.people.track_charge(30.50, { - * '$time': new Date('jan 1 2012') - * }); - * - * @param {Number} amount The amount of money charged to the current user - * @param {Object} [properties] An associative array of properties associated with the charge - * @param {Function} [callback] If provided, the callback will be called when the server responds - * @deprecated - */ -MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { - if (!_.isNumber(amount)) { - amount = parseFloat(amount); - if (isNaN(amount)) { - console.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); - return; - } - } - - return this.append('$transactions', _.extend({ - '$amount': amount - }, properties), callback); -}); - -/* - * Permanently clear all revenue report transactions from the - * current user's people analytics profile. - * - * ### Usage: - * - * mixpanel.people.clear_charges(); - * - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - * @deprecated - */ -MixpanelPeople.prototype.clear_charges = function(callback) { - return this.set('$transactions', [], callback); -}; - -/* -* Permanently deletes the current people analytics profile from -* Mixpanel (using the current distinct_id). -* -* ### Usage: -* -* // remove the all data you have stored about the current user -* mixpanel.people.delete_user(); -* -*/ -MixpanelPeople.prototype.delete_user = function() { - if (!this._identify_called()) { - console.error('mixpanel.people.delete_user() requires you to call identify() first'); - return; - } - var data = {'$delete': this._mixpanel.get_distinct_id()}; - return this._send_request(data); -}; - -MixpanelPeople.prototype.toString = function() { - return this._mixpanel.toString() + '.people'; -}; - -MixpanelPeople.prototype._send_request = function(data, callback) { - data['$token'] = this._get_config('token'); - data['$distinct_id'] = this._mixpanel.get_distinct_id(); - var device_id = this._mixpanel.get_property('$device_id'); - var user_id = this._mixpanel.get_property('$user_id'); - var had_persisted_distinct_id = this._mixpanel.get_property('$had_persisted_distinct_id'); - if (device_id) { - data['$device_id'] = device_id; - } - if (user_id) { - data['$user_id'] = user_id; - } - if (had_persisted_distinct_id) { - data['$had_persisted_distinct_id'] = had_persisted_distinct_id; - } - - var date_encoded_data = _.encodeDates(data); - - if (!this._identify_called()) { - this._enqueue(data); - if (!_.isUndefined(callback)) { - if (this._get_config('verbose')) { - callback({status: -1, error: null}); - } else { - callback(-1); - } - } - return _.truncate(date_encoded_data, 255); - } - - return this._mixpanel._track_or_batch({ - type: 'people', - data: date_encoded_data, - endpoint: this._get_config('api_host') + '/' + this._get_config('api_routes')['engage'], - batcher: this._mixpanel.request_batchers.people - }, callback); -}; - -MixpanelPeople.prototype._get_config = function(conf_var) { - return this._mixpanel.get_config(conf_var); -}; - -MixpanelPeople.prototype._identify_called = function() { - return this._mixpanel._flags.identify_called === true; -}; - -// Queue up engage operations if identify hasn't been called yet. -MixpanelPeople.prototype._enqueue = function(data) { - if (SET_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); - } else if (SET_ONCE_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); - } else if (UNSET_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); - } else if (ADD_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); - } else if (APPEND_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); - } else if (REMOVE_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, data); - } else if (UNION_ACTION in data) { - this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); - } else { - console.error('Invalid call to _enqueue():', data); - } -}; - -MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { - var _this = this; - var queued_data = _.extend({}, this._mixpanel['persistence'].load_queue(action)); - var action_params = queued_data; - - if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { - _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); - _this._mixpanel['persistence'].save(); - if (queue_to_params_fn) { - action_params = queue_to_params_fn(queued_data); - } - action_method.call(_this, action_params, function(response, data) { - // on bad response, we want to add it back to the queue - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); - } - if (!_.isUndefined(callback)) { - callback(response, data); - } - }); - } -}; - -// Flush queued engage operations - order does not matter, -// and there are network level race conditions anyway -MixpanelPeople.prototype._flush = function( - _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback -) { - var _this = this; - - this._flush_one_queue(SET_ACTION, this.set, _set_callback); - this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); - this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); - this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); - this._flush_one_queue(UNION_ACTION, this.union, _union_callback); - - // we have to fire off each $append individually since there is - // no concat method server side - var $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); - if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { - var $append_item; - var append_callback = function(response, data) { - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); - } - if (!_.isUndefined(_append_callback)) { - _append_callback(response, data); - } - }; - for (var i = $append_queue.length - 1; i >= 0; i--) { - $append_queue = this._mixpanel['persistence'].load_queue(APPEND_ACTION); - $append_item = $append_queue.pop(); - _this._mixpanel['persistence'].save(); - if (!_.isEmptyObject($append_item)) { - _this.append($append_item, append_callback); - } - } - } - - // same for $remove - var $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); - if (!_.isUndefined($remove_queue) && _.isArray($remove_queue) && $remove_queue.length) { - var $remove_item; - var remove_callback = function(response, data) { - if (response === 0) { - _this._mixpanel['persistence']._add_to_people_queue(REMOVE_ACTION, $remove_item); - } - if (!_.isUndefined(_remove_callback)) { - _remove_callback(response, data); - } - }; - for (var j = $remove_queue.length - 1; j >= 0; j--) { - $remove_queue = this._mixpanel['persistence'].load_queue(REMOVE_ACTION); - $remove_item = $remove_queue.pop(); - _this._mixpanel['persistence'].save(); - if (!_.isEmptyObject($remove_item)) { - _this.remove($remove_item, remove_callback); - } - } - } -}; - -MixpanelPeople.prototype._is_reserved_property = function(prop) { - return prop === '$distinct_id' || prop === '$token' || prop === '$device_id' || prop === '$user_id' || prop === '$had_persisted_distinct_id'; -}; - -// MixpanelPeople Exports -MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; -MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; -MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; -MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; -MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; -MixpanelPeople.prototype['remove'] = MixpanelPeople.prototype.remove; -MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; -MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; -MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; -MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; -MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; - -/* eslint camelcase: "off" */ - -/* - * Constants - */ -/** @const */ var SET_QUEUE_KEY = '__mps'; -/** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; -/** @const */ var UNSET_QUEUE_KEY = '__mpus'; -/** @const */ var ADD_QUEUE_KEY = '__mpa'; -/** @const */ var APPEND_QUEUE_KEY = '__mpap'; -/** @const */ var REMOVE_QUEUE_KEY = '__mpr'; -/** @const */ var UNION_QUEUE_KEY = '__mpu'; -// This key is deprecated, but we want to check for it to see whether aliasing is allowed. -/** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; -/** @const */ var ALIAS_ID_KEY = '__alias'; -/** @const */ var EVENT_TIMERS_KEY = '__timers'; -/** @const */ var RESERVED_PROPERTIES = [ - SET_QUEUE_KEY, - SET_ONCE_QUEUE_KEY, - UNSET_QUEUE_KEY, - ADD_QUEUE_KEY, - APPEND_QUEUE_KEY, - REMOVE_QUEUE_KEY, - UNION_QUEUE_KEY, - PEOPLE_DISTINCT_ID_KEY, - ALIAS_ID_KEY, - EVENT_TIMERS_KEY -]; - -/** - * Mixpanel Persistence Object - * @constructor - */ -var MixpanelPersistence = function(config) { - this['props'] = {}; - this.campaign_params_saved = false; - - if (config['persistence_name']) { - this.name = 'mp_' + config['persistence_name']; - } else { - this.name = 'mp_' + config['token'] + '_mixpanel'; - } - - var storage_type = config['persistence']; - if (storage_type !== 'cookie' && storage_type !== 'localStorage') { - console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); - storage_type = config['persistence'] = 'cookie'; - } - - if (storage_type === 'localStorage' && _.localStorage.is_supported()) { - this.storage = _.localStorage; - } else { - this.storage = _.cookie; - } - - this.load(); - this.update_config(config); - this.upgrade(); - this.save(); -}; - -MixpanelPersistence.prototype.properties = function() { - var p = {}; - - this.load(); - - // Filter out reserved properties - _.each(this['props'], function(v, k) { - if (!_.include(RESERVED_PROPERTIES, k)) { - p[k] = v; - } - }); - return p; -}; - -MixpanelPersistence.prototype.load = function() { - if (this.disabled) { return; } - - var entry = this.storage.parse(this.name); - - if (entry) { - this['props'] = _.extend({}, entry); - } -}; - -MixpanelPersistence.prototype.upgrade = function() { - var old_cookie, - old_localstorage; - - // if transferring from cookie to localStorage or vice-versa, copy existing - // super properties over to new storage mode - if (this.storage === _.localStorage) { - old_cookie = _.cookie.parse(this.name); - - _.cookie.remove(this.name); - _.cookie.remove(this.name, true); - - if (old_cookie) { - this.register_once(old_cookie); - } - } else if (this.storage === _.cookie) { - old_localstorage = _.localStorage.parse(this.name); - - _.localStorage.remove(this.name); - - if (old_localstorage) { - this.register_once(old_localstorage); - } - } -}; - -MixpanelPersistence.prototype.save = function() { - if (this.disabled) { return; } - - this.storage.set( - this.name, - _.JSONEncode(this['props']), - this.expire_days, - this.cross_subdomain, - this.secure, - this.cross_site, - this.cookie_domain - ); -}; - -MixpanelPersistence.prototype.load_prop = function(key) { - this.load(); - return this['props'][key]; -}; - -MixpanelPersistence.prototype.remove = function() { - // remove both domain and subdomain cookies - this.storage.remove(this.name, false, this.cookie_domain); - this.storage.remove(this.name, true, this.cookie_domain); -}; - -// removes the storage entry and deletes all loaded data -// forced name for tests -MixpanelPersistence.prototype.clear = function() { - this.remove(); - this['props'] = {}; -}; - -/** -* @param {Object} props -* @param {*=} default_value -* @param {number=} days -*/ -MixpanelPersistence.prototype.register_once = function(props, default_value, days) { - if (_.isObject(props)) { - if (typeof(default_value) === 'undefined') { default_value = 'None'; } - this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; - - this.load(); - - _.each(props, function(val, prop) { - if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { - this['props'][prop] = val; - } - }, this); - - this.save(); - - return true; - } - return false; -}; - -/** -* @param {Object} props -* @param {number=} days -*/ -MixpanelPersistence.prototype.register = function(props, days) { - if (_.isObject(props)) { - this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; - - this.load(); - _.extend(this['props'], props); - this.save(); - - return true; - } - return false; -}; - -MixpanelPersistence.prototype.unregister = function(prop) { - this.load(); - if (prop in this['props']) { - delete this['props'][prop]; - this.save(); - } -}; - -MixpanelPersistence.prototype.update_search_keyword = function(referrer) { - this.register(_.info.searchInfo(referrer)); -}; - -// EXPORTED METHOD, we test this directly. -MixpanelPersistence.prototype.update_referrer_info = function(referrer) { - // If referrer doesn't exist, we want to note the fact that it was type-in traffic. - this.register_once({ - '$initial_referrer': referrer || '$direct', - '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' - }, ''); -}; - -MixpanelPersistence.prototype.get_referrer_info = function() { - return _.strip_empty_properties({ - '$initial_referrer': this['props']['$initial_referrer'], - '$initial_referring_domain': this['props']['$initial_referring_domain'] - }); -}; - -MixpanelPersistence.prototype.update_config = function(config) { - this.default_expiry = this.expire_days = config['cookie_expiration']; - this.set_disabled(config['disable_persistence']); - this.set_cookie_domain(config['cookie_domain']); - this.set_cross_site(config['cross_site_cookie']); - this.set_cross_subdomain(config['cross_subdomain_cookie']); - this.set_secure(config['secure_cookie']); -}; - -MixpanelPersistence.prototype.set_disabled = function(disabled) { - this.disabled = disabled; - if (this.disabled) { - this.remove(); - } else { - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cookie_domain = function(cookie_domain) { - if (cookie_domain !== this.cookie_domain) { - this.remove(); - this.cookie_domain = cookie_domain; - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cross_site = function(cross_site) { - if (cross_site !== this.cross_site) { - this.cross_site = cross_site; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { - if (cross_subdomain !== this.cross_subdomain) { - this.cross_subdomain = cross_subdomain; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype.get_cross_subdomain = function() { - return this.cross_subdomain; -}; - -MixpanelPersistence.prototype.set_secure = function(secure) { - if (secure !== this.secure) { - this.secure = secure ? true : false; - this.remove(); - this.save(); - } -}; - -MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { - var q_key = this._get_queue_key(queue), - q_data = data[queue], - set_q = this._get_or_create_queue(SET_ACTION), - set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), - unset_q = this._get_or_create_queue(UNSET_ACTION), - add_q = this._get_or_create_queue(ADD_ACTION), - union_q = this._get_or_create_queue(UNION_ACTION), - remove_q = this._get_or_create_queue(REMOVE_ACTION, []), - append_q = this._get_or_create_queue(APPEND_ACTION, []); - - if (q_key === SET_QUEUE_KEY) { - // Update the set queue - we can override any existing values - _.extend(set_q, q_data); - // if there was a pending increment, override it - // with the set. - this._pop_from_people_queue(ADD_ACTION, q_data); - // if there was a pending union, override it - // with the set. - this._pop_from_people_queue(UNION_ACTION, q_data); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === SET_ONCE_QUEUE_KEY) { - // only queue the data if there is not already a set_once call for it. - _.each(q_data, function(v, k) { - if (!(k in set_once_q)) { - set_once_q[k] = v; - } - }); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === UNSET_QUEUE_KEY) { - _.each(q_data, function(prop) { - - // undo previously-queued actions on this key - _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { - if (prop in enqueued_obj) { - delete enqueued_obj[prop]; - } - }); - _.each(append_q, function(append_obj) { - if (prop in append_obj) { - delete append_obj[prop]; - } - }); - - unset_q[prop] = true; - - }); - } else if (q_key === ADD_QUEUE_KEY) { - _.each(q_data, function(v, k) { - // If it exists in the set queue, increment - // the value - if (k in set_q) { - set_q[k] += v; - } else { - // If it doesn't exist, update the add - // queue - if (!(k in add_q)) { - add_q[k] = 0; - } - add_q[k] += v; - } - }, this); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === UNION_QUEUE_KEY) { - _.each(q_data, function(v, k) { - if (_.isArray(v)) { - if (!(k in union_q)) { - union_q[k] = []; - } - // We may send duplicates, the server will dedup them. - union_q[k] = union_q[k].concat(v); - } - }); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } else if (q_key === REMOVE_QUEUE_KEY) { - remove_q.push(q_data); - this._pop_from_people_queue(APPEND_ACTION, q_data); - } else if (q_key === APPEND_QUEUE_KEY) { - append_q.push(q_data); - this._pop_from_people_queue(UNSET_ACTION, q_data); - } - - console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); - console.log(data); - - this.save(); -}; - -MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { - var q = this['props'][this._get_queue_key(queue)]; - if (!_.isUndefined(q)) { - _.each(data, function(v, k) { - if (queue === APPEND_ACTION || queue === REMOVE_ACTION) { - // list actions: only remove if both k+v match - // e.g. remove should not override append in a case like - // append({foo: 'bar'}); remove({foo: 'qux'}) - _.each(q, function(queued_action) { - if (queued_action[k] === v) { - delete queued_action[k]; - } - }); - } else { - delete q[k]; - } - }, this); - } -}; - -MixpanelPersistence.prototype.load_queue = function(queue) { - return this.load_prop(this._get_queue_key(queue)); -}; - -MixpanelPersistence.prototype._get_queue_key = function(queue) { - if (queue === SET_ACTION) { - return SET_QUEUE_KEY; - } else if (queue === SET_ONCE_ACTION) { - return SET_ONCE_QUEUE_KEY; - } else if (queue === UNSET_ACTION) { - return UNSET_QUEUE_KEY; - } else if (queue === ADD_ACTION) { - return ADD_QUEUE_KEY; - } else if (queue === APPEND_ACTION) { - return APPEND_QUEUE_KEY; - } else if (queue === REMOVE_ACTION) { - return REMOVE_QUEUE_KEY; - } else if (queue === UNION_ACTION) { - return UNION_QUEUE_KEY; - } else { - console.error('Invalid queue:', queue); - } -}; - -MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { - var key = this._get_queue_key(queue); - default_val = _.isUndefined(default_val) ? {} : default_val; - return this['props'][key] || (this['props'][key] = default_val); -}; - -MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { - var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; - timers[event_name] = timestamp; - this['props'][EVENT_TIMERS_KEY] = timers; - this.save(); -}; - -MixpanelPersistence.prototype.remove_event_timer = function(event_name) { - var timers = this.load_prop(EVENT_TIMERS_KEY) || {}; - var timestamp = timers[event_name]; - if (!_.isUndefined(timestamp)) { - delete this['props'][EVENT_TIMERS_KEY][event_name]; - this.save(); - } - return timestamp; -}; - -/* eslint camelcase: "off" */ - -/* - * Mixpanel JS Library - * - * Copyright 2012, Mixpanel, Inc. All Rights Reserved - * http://mixpanel.com/ - * - * Includes portions of Underscore.js - * http://documentcloud.github.com/underscore/ - * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. - * Released under the MIT License. - */ - -// ==ClosureCompiler== -// @compilation_level ADVANCED_OPTIMIZATIONS -// @output_file_name mixpanel-2.8.min.js -// ==/ClosureCompiler== - -/* -SIMPLE STYLE GUIDE: - -this.x === public function -this._x === internal - only use within this file -this.__x === private - only use within the class - -Globals should be all caps -*/ - -var init_type; // MODULE or SNIPPET loader -// allow bundlers to specify how extra code (recorder bundle) should be loaded -// eslint-disable-next-line no-unused-vars -var load_extra_bundle = function(src, _onload) { - throw new Error(src + ' not available in this build.'); -}; - -var mixpanel_master; // main mixpanel instance / object -var INIT_MODULE = 0; -var INIT_SNIPPET = 1; - -var IDENTITY_FUNC = function(x) {return x;}; -var NOOP_FUNC = function() {}; - -/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; -/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64'; -/** @const */ var PAYLOAD_TYPE_JSON = 'json'; -/** @const */ var DEVICE_ID_PREFIX = '$device:'; - - -/* - * Dynamic... constants? Is that an oxymoron? - */ -// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ -// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials -var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); - -// IE<10 does not support cross-origin XHR's but script tags -// with defer won't block window.onload; ENQUEUE_REQUESTS -// should only be true for Opera<12 -var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); - -// save reference to navigator.sendBeacon so it can be minified -var sendBeacon = null; -if (navigator['sendBeacon']) { - sendBeacon = function() { - // late reference to navigator.sendBeacon to allow patching/spying - return navigator['sendBeacon'].apply(navigator, arguments); - }; -} - -var DEFAULT_API_ROUTES = { - 'track': 'track/', - 'engage': 'engage/', - 'groups': 'groups/', - 'record': 'record/' -}; - -/* - * Module-level globals - */ -var DEFAULT_CONFIG = { - 'api_host': 'https://api-js.mixpanel.com', - 'api_routes': DEFAULT_API_ROUTES, - 'api_method': 'POST', - 'api_transport': 'XHR', - 'api_payload_format': PAYLOAD_TYPE_BASE64, - 'app_host': 'https://mixpanel.com', - 'cdn': 'https://cdn.mxpnl.com', - 'cross_site_cookie': false, - 'cross_subdomain_cookie': true, - 'error_reporter': NOOP_FUNC, - 'persistence': 'cookie', - 'persistence_name': '', - 'cookie_domain': '', - 'cookie_name': '', - 'loaded': NOOP_FUNC, - 'mp_loader': null, - 'track_marketing': true, - 'track_pageview': false, - 'skip_first_touch_marketing': false, - 'store_google': true, - 'stop_utm_persistence': false, - 'save_referrer': true, - 'test': false, - 'verbose': false, - 'img': false, - 'debug': false, - 'track_links_timeout': 300, - 'cookie_expiration': 365, - 'upgrade': false, - 'disable_persistence': false, - 'disable_cookie': false, - 'secure_cookie': false, - 'ip': true, - 'opt_out_tracking_by_default': false, - 'opt_out_persistence_by_default': false, - 'opt_out_tracking_persistence_type': 'localStorage', - 'opt_out_tracking_cookie_prefix': null, - 'property_blacklist': [], - 'xhr_headers': {}, // { header: value, header2: value } - 'ignore_dnt': false, - 'batch_requests': true, - 'batch_size': 50, - 'batch_flush_interval_ms': 5000, - 'batch_request_timeout_ms': 90000, - 'batch_autostart': true, - 'hooks': {}, - 'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'), - 'record_block_selector': 'img, video', - 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes - 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'), - 'record_mask_text_selector': '*', - 'record_max_ms': MAX_RECORDING_MS, - 'record_sessions_percent': 0, - 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js' -}; - -var DOM_LOADED = false; - -/** - * Mixpanel Library Object - * @constructor - */ -var MixpanelLib = function() {}; - - -/** - * create_mplib(token:string, config:object, name:string) - * - * This function is used by the init method of MixpanelLib objects - * as well as the main initializer at the end of the JSLib (that - * initializes document.mixpanel as well as any additional instances - * declared before this file has loaded). - */ -var create_mplib = function(token, config, name) { - var instance, - target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; - - if (target && init_type === INIT_MODULE) { - instance = target; - } else { - if (target && !_.isArray(target)) { - console.error('You have already initialized ' + name); - return; - } - instance = new MixpanelLib(); - } - - instance._cached_groups = {}; // cache groups in a pool - - instance._init(token, config, name); - - instance['people'] = new MixpanelPeople(); - instance['people']._init(instance); - - if (!instance.get_config('skip_first_touch_marketing')) { - // We need null UTM params in the object because - // UTM parameters act as a tuple. If any UTM param - // is present, then we set all UTM params including - // empty ones together - var utm_params = _.info.campaignParams(null); - var initial_utm_params = {}; - var has_utm = false; - _.each(utm_params, function(utm_value, utm_key) { - initial_utm_params['initial_' + utm_key] = utm_value; - if (utm_value) { - has_utm = true; - } - }); - if (has_utm) { - instance['people'].set_once(initial_utm_params); - } - } - - // if any instance on the page has debug = true, we set the - // global debug to be true - Config.DEBUG = Config.DEBUG || instance.get_config('debug'); - - // if target is not defined, we called init after the lib already - // loaded, so there won't be an array of things to execute - if (!_.isUndefined(target) && _.isArray(target)) { - // Crunch through the people queue first - we queue this data up & - // flush on identify, so it's better to do all these operations first - instance._execute_array.call(instance['people'], target['people']); - instance._execute_array(target); - } - - return instance; -}; - -// Initialization methods - -/** - * This function initializes a new instance of the Mixpanel tracking object. - * All new instances are added to the main mixpanel object as sub properties (such as - * mixpanel.library_name) and also returned by this function. To define a - * second instance on the page, you would call: - * - * mixpanel.init('new token', { your: 'config' }, 'library_name'); - * - * and use it like so: - * - * mixpanel.library_name.track(...); - * - * @param {String} token Your Mixpanel API token - * @param {Object} [config] A dictionary of config options to override. See a list of default config options. - * @param {String} [name] The name for the new mixpanel instance that you want created - */ -MixpanelLib.prototype.init = function (token, config, name) { - if (_.isUndefined(name)) { - this.report_error('You must name your new library: init(token, config, name)'); - return; - } - if (name === PRIMARY_INSTANCE_NAME) { - this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); - return; - } - - var instance = create_mplib(token, config, name); - mixpanel_master[name] = instance; - instance._loaded(); - - return instance; -}; - -// mixpanel._init(token:string, config:object, name:string) -// -// This function sets up the current instance of the mixpanel -// library. The difference between this method and the init(...) -// method is this one initializes the actual instance, whereas the -// init(...) method sets up a new library and calls _init on it. -// -MixpanelLib.prototype._init = function(token, config, name) { - config = config || {}; - - this['__loaded'] = true; - this['config'] = {}; - - var variable_features = {}; - - // default to JSON payload for standard mixpanel.com API hosts - if (!('api_payload_format' in config)) { - var api_host = config['api_host'] || DEFAULT_CONFIG['api_host']; - if (api_host.match(/\.mixpanel\.com/)) { - variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON; - } - } - - this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, { - 'name': name, - 'token': token, - 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' - })); - - this['_jsc'] = NOOP_FUNC; - - this.__dom_loaded_queue = []; - this.__request_queue = []; - this.__disabled_events = []; - this._flags = { - 'disable_all_events': false, - 'identify_called': false - }; - - // set up request queueing/batching - this.request_batchers = {}; - this._batch_requests = this.get_config('batch_requests'); - if (this._batch_requests) { - if (!_.localStorage.is_supported(true) || !USE_XHR) { - this._batch_requests = false; - console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support'); - _.each(this.get_batcher_configs(), function(batcher_config) { - console.log('Clearing batch queue ' + batcher_config.queue_key); - _.localStorage.remove(batcher_config.queue_key); - }); - } else { - this.init_batchers(); - if (sendBeacon && win.addEventListener) { - // Before page closes or hides (user tabs away etc), attempt to flush any events - // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure, - // events will not be removed from the persistent store; if the site is loaded again, - // the events will be flushed again on startup and deduplicated on the Mixpanel server - // side. - // There is no reliable way to capture only page close events, so we lean on the - // visibilitychange and pagehide events as recommended at - // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes. - // These events fire when the user clicks away from the current page/tab, so will occur - // more frequently than page unload, but are the only mechanism currently for capturing - // this scenario somewhat reliably. - var flush_on_unload = _.bind(function() { - if (!this.request_batchers.events.stopped) { - this.request_batchers.events.flush({unloading: true}); - } - }, this); - win.addEventListener('pagehide', function(ev) { - if (ev['persisted']) { - flush_on_unload(); - } - }); - win.addEventListener('visibilitychange', function() { - if (document$1['visibilityState'] === 'hidden') { - flush_on_unload(); - } - }); - } - } - } - - this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); - this.unpersisted_superprops = {}; - this._gdpr_init(); - - var uuid = _.UUID(); - if (!this.get_distinct_id()) { - // There is no need to set the distinct id - // or the device id if something was already stored - // in the persitence - this.register_once({ - 'distinct_id': DEVICE_ID_PREFIX + uuid, - '$device_id': uuid - }, ''); - } - - var track_pageview_option = this.get_config('track_pageview'); - if (track_pageview_option) { - this._init_url_change_tracking(track_pageview_option); - } - - if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) { - this.start_session_recording(); - } -}; - -MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () { - if (!win['MutationObserver']) { - console.critical('Browser does not support MutationObserver; skipping session recording'); - return; - } - - var handleLoadedRecorder = _.bind(function() { - this._recorder = this._recorder || new win['__mp_recorder'](this); - this._recorder['startRecording'](); - }, this); - - if (_.isUndefined(win['__mp_recorder'])) { - load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder); - } else { - handleLoadedRecorder(); - } -}); - -MixpanelLib.prototype.stop_session_recording = function () { - if (this._recorder) { - this._recorder['stopRecording'](); - } else { - console.critical('Session recorder module not loaded'); - } -}; - -MixpanelLib.prototype.get_session_recording_properties = function () { - var props = {}; - if (this._recorder) { - var replay_id = this._recorder['replayId']; - if (replay_id) { - props['$mp_replay_id'] = replay_id; - } - } - return props; -}; - -// Private methods - -MixpanelLib.prototype._loaded = function() { - this.get_config('loaded')(this); - this._set_default_superprops(); - this['people'].set_once(this['persistence'].get_referrer_info()); - - // `store_google` is now deprecated and previously stored UTM parameters are cleared - // from persistence by default. - if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) { - var utm_params = _.info.campaignParams(null); - _.each(utm_params, function(_utm_value, utm_key) { - // We need to unregister persisted UTM parameters so old values - // are not mixed with the new UTM parameters - this.unregister(utm_key); - }.bind(this)); - } -}; - -// update persistence with info on referrer, UTM params, etc -MixpanelLib.prototype._set_default_superprops = function() { - this['persistence'].update_search_keyword(document$1.referrer); - // Registering super properties for UTM persistence by 'store_google' is deprecated. - if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) { - this.register(_.info.campaignParams()); - } - if (this.get_config('save_referrer')) { - this['persistence'].update_referrer_info(document$1.referrer); - } -}; - -MixpanelLib.prototype._dom_loaded = function() { - _.each(this.__dom_loaded_queue, function(item) { - this._track_dom.apply(this, item); - }, this); - - if (!this.has_opted_out_tracking()) { - _.each(this.__request_queue, function(item) { - this._send_request.apply(this, item); - }, this); - } - - delete this.__dom_loaded_queue; - delete this.__request_queue; -}; - -MixpanelLib.prototype._track_dom = function(DomClass, args) { - if (this.get_config('img')) { - this.report_error('You can\'t use DOM tracking functions with img = true.'); - return false; - } - - if (!DOM_LOADED) { - this.__dom_loaded_queue.push([DomClass, args]); - return false; - } - - var dt = new DomClass().init(this); - return dt.track.apply(dt, args); -}; - -MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) { - var previous_tracked_url = ''; - var tracked = this.track_pageview(); - if (tracked) { - previous_tracked_url = _.info.currentUrl(); - } - - if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) { - win.addEventListener('popstate', function() { - win.dispatchEvent(new Event('mp_locationchange')); - }); - win.addEventListener('hashchange', function() { - win.dispatchEvent(new Event('mp_locationchange')); - }); - var nativePushState = win.history.pushState; - if (typeof nativePushState === 'function') { - win.history.pushState = function(state, unused, url) { - nativePushState.call(win.history, state, unused, url); - win.dispatchEvent(new Event('mp_locationchange')); - }; - } - var nativeReplaceState = win.history.replaceState; - if (typeof nativeReplaceState === 'function') { - win.history.replaceState = function(state, unused, url) { - nativeReplaceState.call(win.history, state, unused, url); - win.dispatchEvent(new Event('mp_locationchange')); - }; - } - win.addEventListener('mp_locationchange', function() { - var current_url = _.info.currentUrl(); - var should_track = false; - if (track_pageview_option === 'full-url') { - should_track = current_url !== previous_tracked_url; - } else if (track_pageview_option === 'url-with-path-and-query-string') { - should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0]; - } else if (track_pageview_option === 'url-with-path') { - should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0]; - } - - if (should_track) { - var tracked = this.track_pageview(); - if (tracked) { - previous_tracked_url = current_url; - } - } - }.bind(this)); - } -}; - -/** - * _prepare_callback() should be called by callers of _send_request for use - * as the callback argument. - * - * If there is no callback, this returns null. - * If we are going to make XHR/XDR requests, this returns a function. - * If we are going to use script tags, this returns a string to use as the - * callback GET param. - */ -MixpanelLib.prototype._prepare_callback = function(callback, data) { - if (_.isUndefined(callback)) { - return null; - } - - if (USE_XHR) { - var callback_function = function(response) { - callback(response, data); - }; - return callback_function; - } else { - // if the user gives us a callback, we store as a random - // property on this instances jsc function and update our - // callback string to reflect that. - var jsc = this['_jsc']; - var randomized_cb = '' + Math.floor(Math.random() * 100000000); - var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; - jsc[randomized_cb] = function(response) { - delete jsc[randomized_cb]; - callback(response, data); - }; - return callback_string; - } -}; - -MixpanelLib.prototype._send_request = function(url, data, options, callback) { - var succeeded = true; - - if (ENQUEUE_REQUESTS) { - this.__request_queue.push(arguments); - return succeeded; - } - - var DEFAULT_OPTIONS = { - method: this.get_config('api_method'), - transport: this.get_config('api_transport'), - verbose: this.get_config('verbose') - }; - var body_data = null; - - if (!callback && (_.isFunction(options) || typeof options === 'string')) { - callback = options; - options = null; - } - options = _.extend(DEFAULT_OPTIONS, options || {}); - if (!USE_XHR) { - options.method = 'GET'; - } - var use_post = options.method === 'POST'; - var use_sendBeacon = sendBeacon && use_post && options.transport.toLowerCase() === 'sendbeacon'; - - // needed to correctly format responses - var verbose_mode = options.verbose; - if (data['verbose']) { verbose_mode = true; } - - if (this.get_config('test')) { data['test'] = 1; } - if (verbose_mode) { data['verbose'] = 1; } - if (this.get_config('img')) { data['img'] = 1; } - if (!USE_XHR) { - if (callback) { - data['callback'] = callback; - } else if (verbose_mode || this.get_config('test')) { - // Verbose output (from verbose mode, or an error in test mode) is a json blob, - // which by itself is not valid javascript. Without a callback, this verbose output will - // cause an error when returned via jsonp, so we force a no-op callback param. - // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 - data['callback'] = '(function(){})'; - } - } - - data['ip'] = this.get_config('ip')?1:0; - data['_'] = new Date().getTime().toString(); - - if (use_post) { - body_data = 'data=' + encodeURIComponent(data['data']); - delete data['data']; - } - - url += '?' + _.HTTPBuildQuery(data); - - var lib = this; - if ('img' in data) { - var img = document$1.createElement('img'); - img.src = url; - document$1.body.appendChild(img); - } else if (use_sendBeacon) { - try { - succeeded = sendBeacon(url, body_data); - } catch (e) { - lib.report_error(e); - succeeded = false; - } - try { - if (callback) { - callback(succeeded ? 1 : 0); - } - } catch (e) { - lib.report_error(e); - } - } else if (USE_XHR) { - try { - var req = new XMLHttpRequest(); - req.open(options.method, url, true); - - var headers = this.get_config('xhr_headers'); - if (use_post) { - headers['Content-Type'] = 'application/x-www-form-urlencoded'; - } - _.each(headers, function(headerValue, headerName) { - req.setRequestHeader(headerName, headerValue); - }); - - if (options.timeout_ms && typeof req.timeout !== 'undefined') { - req.timeout = options.timeout_ms; - var start_time = new Date().getTime(); - } - - // send the mp_optout cookie - // withCredentials cannot be modified until after calling .open on Android and Mobile Safari - req.withCredentials = true; - req.onreadystatechange = function () { - if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 - if (req.status === 200) { - if (callback) { - if (verbose_mode) { - var response; - try { - response = _.JSONDecode(req.responseText); - } catch (e) { - lib.report_error(e); - if (options.ignore_json_errors) { - response = req.responseText; - } else { - return; - } - } - callback(response); - } else { - callback(Number(req.responseText)); - } - } - } else { - var error; - if ( - req.timeout && - !req.status && - new Date().getTime() - start_time >= req.timeout - ) { - error = 'timeout'; - } else { - error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; - } - lib.report_error(error); - if (callback) { - if (verbose_mode) { - callback({status: 0, error: error, xhr_req: req}); - } else { - callback(0); - } - } - } - } - }; - req.send(body_data); - } catch (e) { - lib.report_error(e); - succeeded = false; - } - } else { - var script = document$1.createElement('script'); - script.type = 'text/javascript'; - script.async = true; - script.defer = true; - script.src = url; - var s = document$1.getElementsByTagName('script')[0]; - s.parentNode.insertBefore(script, s); - } - - return succeeded; -}; - -/** - * _execute_array() deals with processing any mixpanel function - * calls that were called before the Mixpanel library were loaded - * (and are thus stored in an array so they can be called later) - * - * Note: we fire off all the mixpanel function calls && user defined - * functions BEFORE we fire off mixpanel tracking calls. This is so - * identify/register/set_config calls can properly modify early - * tracking calls. - * - * @param {Array} array - */ -MixpanelLib.prototype._execute_array = function(array) { - var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; - _.each(array, function(item) { - if (item) { - fn_name = item[0]; - if (_.isArray(fn_name)) { - tracking_calls.push(item); // chained call e.g. mixpanel.get_group().set() - } else if (typeof(item) === 'function') { - item.call(this); - } else if (_.isArray(item) && fn_name === 'alias') { - alias_calls.push(item); - } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { - tracking_calls.push(item); - } else { - other_calls.push(item); - } - } - }, this); - - var execute = function(calls, context) { - _.each(calls, function(item) { - if (_.isArray(item[0])) { - // chained call - var caller = context; - _.each(item, function(call) { - caller = caller[call[0]].apply(caller, call.slice(1)); - }); - } else { - this[item[0]].apply(this, item.slice(1)); - } - }, context); - }; - - execute(alias_calls, this); - execute(other_calls, this); - execute(tracking_calls, this); -}; - -// request queueing utils - -MixpanelLib.prototype.are_batchers_initialized = function() { - return !!this.request_batchers.events; -}; - -MixpanelLib.prototype.get_batcher_configs = function() { - var queue_prefix = '__mpq_' + this.get_config('token'); - var api_routes = this.get_config('api_routes'); - this._batcher_configs = this._batcher_configs || { - events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'}, - people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'}, - groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'} - }; - return this._batcher_configs; -}; - -MixpanelLib.prototype.init_batchers = function() { - if (!this.are_batchers_initialized()) { - var batcher_for = _.bind(function(attrs) { - return new RequestBatcher( - attrs.queue_key, - { - libConfig: this['config'], - sendRequestFunc: _.bind(function(data, options, cb) { - this._send_request( - this.get_config('api_host') + attrs.endpoint, - this._encode_data_for_request(data), - options, - this._prepare_callback(cb, data) - ); - }, this), - beforeSendHook: _.bind(function(item) { - return this._run_hook('before_send_' + attrs.type, item); - }, this), - errorReporter: this.get_config('error_reporter'), - stopAllBatchingFunc: _.bind(this.stop_batch_senders, this) - } - ); - }, this); - var batcher_configs = this.get_batcher_configs(); - this.request_batchers = { - events: batcher_for(batcher_configs.events), - people: batcher_for(batcher_configs.people), - groups: batcher_for(batcher_configs.groups) - }; - } - if (this.get_config('batch_autostart')) { - this.start_batch_senders(); - } -}; - -MixpanelLib.prototype.start_batch_senders = function() { - this._batchers_were_started = true; - if (this.are_batchers_initialized()) { - this._batch_requests = true; - _.each(this.request_batchers, function(batcher) { - batcher.start(); - }); - } -}; - -MixpanelLib.prototype.stop_batch_senders = function() { - this._batch_requests = false; - _.each(this.request_batchers, function(batcher) { - batcher.stop(); - batcher.clear(); - }); -}; - -/** - * push() keeps the standard async-array-push - * behavior around after the lib is loaded. - * This is only useful for external integrations that - * do not wish to rely on our convenience methods - * (created in the snippet). - * - * ### Usage: - * mixpanel.push(['register', { a: 'b' }]); - * - * @param {Array} item A [function_name, args...] array to be executed - */ -MixpanelLib.prototype.push = function(item) { - this._execute_array([item]); -}; - -/** - * Disable events on the Mixpanel object. If passed no arguments, - * this function disables tracking of any event. If passed an - * array of event names, those events will be disabled, but other - * events will continue to be tracked. - * - * Note: this function does not stop other mixpanel functions from - * firing, such as register() or people.set(). - * - * @param {Array} [events] An array of event names to disable - */ -MixpanelLib.prototype.disable = function(events) { - if (typeof(events) === 'undefined') { - this._flags.disable_all_events = true; - } else { - this.__disabled_events = this.__disabled_events.concat(events); - } -}; - -MixpanelLib.prototype._encode_data_for_request = function(data) { - var encoded_data = _.JSONEncode(data); - if (this.get_config('api_payload_format') === PAYLOAD_TYPE_BASE64) { - encoded_data = _.base64Encode(encoded_data); - } - return {'data': encoded_data}; -}; - -// internal method for handling track vs batch-enqueue logic -MixpanelLib.prototype._track_or_batch = function(options, callback) { - var truncated_data = _.truncate(options.data, 255); - var endpoint = options.endpoint; - var batcher = options.batcher; - var should_send_immediately = options.should_send_immediately; - var send_request_options = options.send_request_options || {}; - callback = callback || NOOP_FUNC; - - var request_enqueued_or_initiated = true; - var send_request_immediately = _.bind(function() { - if (!send_request_options.skip_hooks) { - truncated_data = this._run_hook('before_send_' + options.type, truncated_data); - } - if (truncated_data) { - console.log('MIXPANEL REQUEST:'); - console.log(truncated_data); - return this._send_request( - endpoint, - this._encode_data_for_request(truncated_data), - send_request_options, - this._prepare_callback(callback, truncated_data) - ); - } else { - return null; - } - }, this); - - if (this._batch_requests && !should_send_immediately) { - batcher.enqueue(truncated_data, function(succeeded) { - if (succeeded) { - callback(1, truncated_data); - } else { - send_request_immediately(); - } - }); - } else { - request_enqueued_or_initiated = send_request_immediately(); - } - - return request_enqueued_or_initiated && truncated_data; -}; - -/** - * Track an event. This is the most important and - * frequently used Mixpanel function. - * - * ### Usage: - * - * // track an event named 'Registered' - * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); - * - * // track an event using navigator.sendBeacon - * mixpanel.track('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'}); - * - * To track link clicks or form submissions, see track_links() or track_forms(). - * - * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. - * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. - * @param {Object} [options] Optional configuration for this track request. - * @param {String} [options.transport] Transport method for network request ('xhr' or 'sendBeacon'). - * @param {Boolean} [options.send_immediately] Whether to bypass batching/queueing and send track request immediately. - * @param {Function} [callback] If provided, the callback function will be called after tracking the event. - * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object - * with the tracking payload sent to the API server is returned; otherwise false. - */ -MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) { - if (!callback && typeof options === 'function') { - callback = options; - options = null; - } - options = options || {}; - var transport = options['transport']; // external API, don't minify 'transport' prop - if (transport) { - options.transport = transport; // 'transport' prop name can be minified internally - } - var should_send_immediately = options['send_immediately']; - if (typeof callback !== 'function') { - callback = NOOP_FUNC; - } - - if (_.isUndefined(event_name)) { - this.report_error('No event name provided to mixpanel.track'); - return; - } - - if (this._event_is_disabled(event_name)) { - callback(0); - return; - } - - // set defaults - properties = _.extend({}, properties); - properties['token'] = this.get_config('token'); - - // set $duration if time_event was previously called for this event - var start_timestamp = this['persistence'].remove_event_timer(event_name); - if (!_.isUndefined(start_timestamp)) { - var duration_in_ms = new Date().getTime() - start_timestamp; - properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); - } - - this._set_default_superprops(); - - var marketing_properties = this.get_config('track_marketing') - ? _.info.marketingParams() - : {}; - - // note: extend writes to the first object, so lets make sure we - // don't write to the persistence properties object and info - // properties object by passing in a new object - - // update properties with pageview info and super-properties - properties = _.extend( - {}, - _.info.properties({'mp_loader': this.get_config('mp_loader')}), - marketing_properties, - this['persistence'].properties(), - this.unpersisted_superprops, - this.get_session_recording_properties(), - properties - ); - - var property_blacklist = this.get_config('property_blacklist'); - if (_.isArray(property_blacklist)) { - _.each(property_blacklist, function(blacklisted_prop) { - delete properties[blacklisted_prop]; - }); - } else { - this.report_error('Invalid value for property_blacklist config: ' + property_blacklist); - } - - var data = { - 'event': event_name, - 'properties': properties - }; - var ret = this._track_or_batch({ - type: 'events', - data: data, - endpoint: this.get_config('api_host') + '/' + this.get_config('api_routes')['track'], - batcher: this.request_batchers.events, - should_send_immediately: should_send_immediately, - send_request_options: options - }, callback); - - return ret; -}); - -/** - * Register the current user into one/many groups. - * - * ### Usage: - * - * mixpanel.set_group('company', ['mixpanel', 'google']) // an array of IDs - * mixpanel.set_group('company', 'mixpanel') - * mixpanel.set_group('company', 128746312) - * - * @param {String} group_key Group key - * @param {Array|String|Number} group_ids An array of group IDs, or a singular group ID - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - * - */ -MixpanelLib.prototype.set_group = addOptOutCheckMixpanelLib(function(group_key, group_ids, callback) { - if (!_.isArray(group_ids)) { - group_ids = [group_ids]; - } - var prop = {}; - prop[group_key] = group_ids; - this.register(prop); - return this['people'].set(group_key, group_ids, callback); -}); - -/** - * Add a new group for this user. - * - * ### Usage: - * - * mixpanel.add_group('company', 'mixpanel') - * - * @param {String} group_key Group key - * @param {*} group_id A valid Mixpanel property type - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.add_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { - var old_values = this.get_property(group_key); - var prop = {}; - if (old_values === undefined) { - prop[group_key] = [group_id]; - this.register(prop); - } else { - if (old_values.indexOf(group_id) === -1) { - old_values.push(group_id); - prop[group_key] = old_values; - this.register(prop); - } - } - return this['people'].union(group_key, group_id, callback); -}); - -/** - * Remove a group from this user. - * - * ### Usage: - * - * mixpanel.remove_group('company', 'mixpanel') - * - * @param {String} group_key Group key - * @param {*} group_id A valid Mixpanel property type - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.remove_group = addOptOutCheckMixpanelLib(function(group_key, group_id, callback) { - var old_value = this.get_property(group_key); - // if the value doesn't exist, the persistent store is unchanged - if (old_value !== undefined) { - var idx = old_value.indexOf(group_id); - if (idx > -1) { - old_value.splice(idx, 1); - this.register({group_key: old_value}); - } - if (old_value.length === 0) { - this.unregister(group_key); - } - } - return this['people'].remove(group_key, group_id, callback); -}); - -/** - * Track an event with specific groups. - * - * ### Usage: - * - * mixpanel.track_with_groups('purchase', {'product': 'iphone'}, {'University': ['UCB', 'UCLA']}) - * - * @param {String} event_name The name of the event (see `mixpanel.track()`) - * @param {Object=} properties A set of properties to include with the event you're sending (see `mixpanel.track()`) - * @param {Object=} groups An object mapping group name keys to one or more values - * @param {Function} [callback] If provided, the callback will be called after tracking the event. - */ -MixpanelLib.prototype.track_with_groups = addOptOutCheckMixpanelLib(function(event_name, properties, groups, callback) { - var tracking_props = _.extend({}, properties || {}); - _.each(groups, function(v, k) { - if (v !== null && v !== undefined) { - tracking_props[k] = v; - } - }); - return this.track(event_name, tracking_props, callback); -}); - -MixpanelLib.prototype._create_map_key = function (group_key, group_id) { - return group_key + '_' + JSON.stringify(group_id); -}; - -MixpanelLib.prototype._remove_group_from_cache = function (group_key, group_id) { - delete this._cached_groups[this._create_map_key(group_key, group_id)]; -}; - -/** - * Look up reference to a Mixpanel group - * - * ### Usage: - * - * mixpanel.get_group(group_key, group_id) - * - * @param {String} group_key Group key - * @param {Object} group_id A valid Mixpanel property type - * @returns {Object} A MixpanelGroup identifier - */ -MixpanelLib.prototype.get_group = function (group_key, group_id) { - var map_key = this._create_map_key(group_key, group_id); - var group = this._cached_groups[map_key]; - if (group === undefined || group._group_key !== group_key || group._group_id !== group_id) { - group = new MixpanelGroup(); - group._init(this, group_key, group_id); - this._cached_groups[map_key] = group; - } - return group; -}; - -/** - * Track a default Mixpanel page view event, which includes extra default event properties to - * improve page view data. - * - * ### Usage: - * - * // track a default $mp_web_page_view event - * mixpanel.track_pageview(); - * - * // track a page view event with additional event properties - * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'}); - * - * // example approach to track page views on different page types as event properties - * mixpanel.track_pageview({'page': 'pricing'}); - * mixpanel.track_pageview({'page': 'homepage'}); - * - * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for - * // individual pages on the same site or product. Use cases for custom event_name may be page - * // views on different products or internal applications that are considered completely separate - * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'}); - * - * ### Notes: - * - * The `config.track_pageview` option for mixpanel.init() - * may be turned on for tracking page loads automatically. - * - * // track only page loads - * mixpanel.init(PROJECT_TOKEN, {track_pageview: true}); - * - * // track when the URL changes in any manner - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'}); - * - * // track when the URL changes, ignoring any changes in the hash part - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'}); - * - * // track when the path changes, ignoring any query parameter or hash changes - * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'}); - * - * @param {Object} [properties] An optional set of additional properties to send with the page view event - * @param {Object} [options] Page view tracking options - * @param {String} [options.event_name] - Alternate name for the tracking event - * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object - * with the tracking payload sent to the API server is returned; otherwise false. - */ -MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) { - if (typeof properties !== 'object') { - properties = {}; - } - options = options || {}; - var event_name = options['event_name'] || '$mp_web_page_view'; - - var default_page_properties = _.extend( - _.info.mpPageViewProperties(), - _.info.campaignParams(), - _.info.clickParams() - ); - - var event_properties = _.extend( - {}, - default_page_properties, - properties - ); - - return this.track(event_name, event_properties); -}); - -/** - * Track clicks on a set of document elements. Selector must be a - * valid query. Elements must exist on the page at the time track_links is called. - * - * ### Usage: - * - * // track click for link id #nav - * mixpanel.track_links('#nav', 'Clicked Nav Link'); - * - * ### Notes: - * - * This function will wait up to 300 ms for the Mixpanel - * servers to respond. If they have not responded by that time - * it will head to the link without ensuring that your event - * has been tracked. To configure this timeout please see the - * set_config() documentation below. - * - * If you pass a function in as the properties argument, the - * function will receive the DOMElement that triggered the - * event as an argument. You are expected to return an object - * from the function; any properties defined on this object - * will be sent to mixpanel as event properties. - * - * @type {Function} - * @param {Object|String} query A valid DOM query, element or jQuery-esque list - * @param {String} event_name The name of the event to track - * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement - */ -MixpanelLib.prototype.track_links = function() { - return this._track_dom.call(this, LinkTracker, arguments); -}; - -/** - * Track form submissions. Selector must be a valid query. - * - * ### Usage: - * - * // track submission for form id 'register' - * mixpanel.track_forms('#register', 'Created Account'); - * - * ### Notes: - * - * This function will wait up to 300 ms for the mixpanel - * servers to respond, if they have not responded by that time - * it will head to the link without ensuring that your event - * has been tracked. To configure this timeout please see the - * set_config() documentation below. - * - * If you pass a function in as the properties argument, the - * function will receive the DOMElement that triggered the - * event as an argument. You are expected to return an object - * from the function; any properties defined on this object - * will be sent to mixpanel as event properties. - * - * @type {Function} - * @param {Object|String} query A valid DOM query, element or jQuery-esque list - * @param {String} event_name The name of the event to track - * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement - */ -MixpanelLib.prototype.track_forms = function() { - return this._track_dom.call(this, FormTracker, arguments); -}; - -/** - * Time an event by including the time between this call and a - * later 'track' call for the same event in the properties sent - * with the event. - * - * ### Usage: - * - * // time an event named 'Registered' - * mixpanel.time_event('Registered'); - * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); - * - * When called for a particular event name, the next track call for that event - * name will include the elapsed time between the 'time_event' and 'track' - * calls. This value is stored as seconds in the '$duration' property. - * - * @param {String} event_name The name of the event. - */ -MixpanelLib.prototype.time_event = function(event_name) { - if (_.isUndefined(event_name)) { - this.report_error('No event name provided to mixpanel.time_event'); - return; - } - - if (this._event_is_disabled(event_name)) { - return; - } - - this['persistence'].set_event_timer(event_name, new Date().getTime()); -}; - -var REGISTER_DEFAULTS = { - 'persistent': true -}; -/** - * Helper to parse options param for register methods, maintaining - * legacy support for plain "days" param instead of options object - * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods - * @returns {Object} options object - */ -var options_for_register = function(days_or_options) { - var options; - if (_.isObject(days_or_options)) { - options = days_or_options; - } else if (!_.isUndefined(days_or_options)) { - options = {'days': days_or_options}; - } else { - options = {}; - } - return _.extend({}, REGISTER_DEFAULTS, options); -}; - -/** - * Register a set of super properties, which are included with all - * events. This will overwrite previous super property values. - * - * ### Usage: - * - * // register 'Gender' as a super property - * mixpanel.register({'Gender': 'Female'}); - * - * // register several super properties when a user signs up - * mixpanel.register({ - * 'Email': 'jdoe@example.com', - * 'Account Type': 'Free' - * }); - * - * // register only for the current pageload - * mixpanel.register({'Name': 'Pat'}, {persistent: false}); - * - * @param {Object} properties An associative array of properties to store about the user - * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.register = function(props, days_or_options) { - var options = options_for_register(days_or_options); - if (options['persistent']) { - this['persistence'].register(props, options['days']); - } else { - _.extend(this.unpersisted_superprops, props); - } -}; - -/** - * Register a set of super properties only once. This will not - * overwrite previous super property values, unlike register(). - * - * ### Usage: - * - * // register a super property for the first time only - * mixpanel.register_once({ - * 'First Login Date': new Date().toISOString() - * }); - * - * // register once, only for the current pageload - * mixpanel.register_once({ - * 'First interaction time': new Date().toISOString() - * }, 'None', {persistent: false}); - * - * ### Notes: - * - * If default_value is specified, current super properties - * with that value will be overwritten. - * - * @param {Object} properties An associative array of properties to store about the user - * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' - * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props) - * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) { - var options = options_for_register(days_or_options); - if (options['persistent']) { - this['persistence'].register_once(props, default_value, options['days']); - } else { - if (typeof(default_value) === 'undefined') { - default_value = 'None'; - } - _.each(props, function(val, prop) { - if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) { - this.unpersisted_superprops[prop] = val; - } - }, this); - } -}; - -/** - * Delete a super property stored with the current user. - * - * @param {String} property The name of the super property to remove - * @param {Object} [options] - * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage) - */ -MixpanelLib.prototype.unregister = function(property, options) { - options = options_for_register(options); - if (options['persistent']) { - this['persistence'].unregister(property); - } else { - delete this.unpersisted_superprops[property]; - } -}; - -MixpanelLib.prototype._register_single = function(prop, value) { - var props = {}; - props[prop] = value; - this.register(props); -}; - -/** - * Identify a user with a unique ID to track user activity across - * devices, tie a user to their events, and create a user profile. - * If you never call this method, unique visitors are tracked using - * a UUID generated the first time they visit the site. - * - * Call identify when you know the identity of the current user, - * typically after login or signup. We recommend against using - * identify for anonymous visitors to your site. - * - * ### Notes: - * If your project has - * ID Merge - * enabled, the identify method will connect pre- and - * post-authentication events when appropriate. - * - * If your project does not have ID Merge enabled, identify will - * change the user's local distinct_id to the unique ID you pass. - * Events tracked prior to authentication will not be connected - * to the same user identity. If ID Merge is disabled, alias can - * be used to connect pre- and post-registration events. - * - * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. - */ -MixpanelLib.prototype.identify = function( - new_distinct_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback -) { - // Optional Parameters - // _set_callback:function A callback to be run if and when the People set queue is flushed - // _add_callback:function A callback to be run if and when the People add queue is flushed - // _append_callback:function A callback to be run if and when the People append queue is flushed - // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed - // _union_callback:function A callback to be run if and when the People union queue is flushed - // _unset_callback:function A callback to be run if and when the People unset queue is flushed - - var previous_distinct_id = this.get_distinct_id(); - if (new_distinct_id && previous_distinct_id !== new_distinct_id) { - // we allow the following condition if previous distinct_id is same as new_distinct_id - // so that you can force flush people updates for anonymous profiles. - if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) { - this.report_error('distinct_id cannot have $device: prefix'); - return -1; - } - this.register({'$user_id': new_distinct_id}); - } - - if (!this.get_property('$device_id')) { - // The persisted distinct id might not actually be a device id at all - // it might be a distinct id of the user from before - var device_id = previous_distinct_id; - this.register_once({ - '$had_persisted_distinct_id': true, - '$device_id': device_id - }, ''); - } - - // identify only changes the distinct id if it doesn't match either the existing or the alias; - // if it's new, blow away the alias as well. - if (new_distinct_id !== previous_distinct_id && new_distinct_id !== this.get_property(ALIAS_ID_KEY)) { - this.unregister(ALIAS_ID_KEY); - this.register({'distinct_id': new_distinct_id}); - } - this._flags.identify_called = true; - // Flush any queued up people requests - this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback); - - // send an $identify event any time the distinct_id is changing - logic on the server - // will determine whether or not to do anything with it. - if (new_distinct_id !== previous_distinct_id) { - this.track('$identify', { - 'distinct_id': new_distinct_id, - '$anon_distinct_id': previous_distinct_id - }, {skip_hooks: true}); - } -}; - -/** - * Clears super properties and generates a new random distinct_id for this instance. - * Useful for clearing data when a user logs out. - */ -MixpanelLib.prototype.reset = function() { - this['persistence'].clear(); - this._flags.identify_called = false; - var uuid = _.UUID(); - this.register_once({ - 'distinct_id': DEVICE_ID_PREFIX + uuid, - '$device_id': uuid - }, ''); -}; - -/** - * Returns the current distinct id of the user. This is either the id automatically - * generated by the library or the id that has been passed by a call to identify(). - * - * ### Notes: - * - * get_distinct_id() can only be called after the Mixpanel library has finished loading. - * init() has a loaded function available to handle this automatically. For example: - * - * // set distinct_id after the mixpanel library has loaded - * mixpanel.init('YOUR PROJECT TOKEN', { - * loaded: function(mixpanel) { - * distinct_id = mixpanel.get_distinct_id(); - * } - * }); - */ -MixpanelLib.prototype.get_distinct_id = function() { - return this.get_property('distinct_id'); -}; - -/** - * The alias method creates an alias which Mixpanel will use to - * remap one id to another. Multiple aliases can point to the - * same identifier. - * - * The following is a valid use of alias: - * - * mixpanel.alias('new_id', 'existing_id'); - * // You can add multiple id aliases to the existing ID - * mixpanel.alias('newer_id', 'existing_id'); - * - * Aliases can also be chained - the following is a valid example: - * - * mixpanel.alias('new_id', 'existing_id'); - * // chain newer_id - new_id - existing_id - * mixpanel.alias('newer_id', 'new_id'); - * - * Aliases cannot point to multiple identifiers - the following - * example will not work: - * - * mixpanel.alias('new_id', 'existing_id'); - * // this is invalid as 'new_id' already points to 'existing_id' - * mixpanel.alias('new_id', 'newer_id'); - * - * ### Notes: - * - * If your project does not have - * ID Merge - * enabled, the best practice is to call alias once when a unique - * ID is first created for a user (e.g., when a user first registers - * for an account). Do not use alias multiple times for a single - * user without ID Merge enabled. - * - * @param {String} alias A unique identifier that you want to use for this user in the future. - * @param {String} [original] The current identifier being used for this user. - */ -MixpanelLib.prototype.alias = function(alias, original) { - // If the $people_distinct_id key exists in persistence, there has been a previous - // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with - // this ID, as it will duplicate users. - if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { - this.report_error('Attempting to create alias for existing People user - aborting.'); - return -2; - } - - var _this = this; - if (_.isUndefined(original)) { - original = this.get_distinct_id(); - } - if (alias !== original) { - this._register_single(ALIAS_ID_KEY, alias); - return this.track('$create_alias', { - 'alias': alias, - 'distinct_id': original - }, { - skip_hooks: true - }, function() { - // Flush the people queue - _this.identify(alias); - }); - } else { - this.report_error('alias matches current distinct_id - skipping api call.'); - this.identify(alias); - return -1; - } -}; - -/** - * Provide a string to recognize the user by. The string passed to - * this method will appear in the Mixpanel Streams product rather - * than an automatically generated name. Name tags do not have to - * be unique. - * - * This value will only be included in Streams data. - * - * @param {String} name_tag A human readable name for the user - * @deprecated - */ -MixpanelLib.prototype.name_tag = function(name_tag) { - this._register_single('mp_name_tag', name_tag); -}; - -/** - * Update the configuration of a mixpanel library instance. - * - * The default config is: - * - * { - * // host for requests (customizable for e.g. a local proxy) - * api_host: 'https://api-js.mixpanel.com', - * - * // endpoints for different types of requests - * api_routes: { - * track: 'track/', - * engage: 'engage/', - * groups: 'groups/', - * } - * - * // HTTP method for tracking requests - * api_method: 'POST' - * - * // transport for sending requests ('XHR' or 'sendBeacon') - * // NB: sendBeacon should only be used for scenarios such as - * // page unload where a "best-effort" attempt to send is - * // acceptable; the sendBeacon API does not support callbacks - * // or any way to know the result of the request. Mixpanel - * // tracking via sendBeacon will not support any event- - * // batching or retry mechanisms. - * api_transport: 'XHR' - * - * // request-batching/queueing/retry - * batch_requests: true, - * - * // maximum number of events/updates to send in a single - * // network request - * batch_size: 50, - * - * // milliseconds to wait between sending batch requests - * batch_flush_interval_ms: 5000, - * - * // milliseconds to wait for network responses to batch requests - * // before they are considered timed-out and retried - * batch_request_timeout_ms: 90000, - * - * // override value for cookie domain, only useful for ensuring - * // correct cross-subdomain cookies on unusual domains like - * // subdomain.mainsite.avocat.fr; NB this cannot be used to - * // set cookies on a different domain than the current origin - * cookie_domain: '' - * - * // super properties cookie expiration (in days) - * cookie_expiration: 365 - * - * // if true, cookie will be set with SameSite=None; Secure - * // this is only useful in special situations, like embedded - * // 3rd-party iframes that set up a Mixpanel instance - * cross_site_cookie: false - * - * // super properties span subdomains - * cross_subdomain_cookie: true - * - * // debug mode - * debug: false - * - * // if this is true, the mixpanel cookie or localStorage entry - * // will be deleted, and no user persistence will take place - * disable_persistence: false - * - * // if this is true, Mixpanel will automatically determine - * // City, Region and Country data using the IP address of - * //the client - * ip: true - * - * // opt users out of tracking by this Mixpanel instance by default - * opt_out_tracking_by_default: false - * - * // opt users out of browser data storage by this Mixpanel instance by default - * opt_out_persistence_by_default: false - * - * // persistence mechanism used by opt-in/opt-out methods - cookie - * // or localStorage - falls back to cookie if localStorage is unavailable - * opt_out_tracking_persistence_type: 'localStorage' - * - * // customize the name of cookie/localStorage set by opt-in/opt-out methods - * opt_out_tracking_cookie_prefix: null - * - * // type of persistent store for super properties (cookie/ - * // localStorage) if set to 'localStorage', any existing - * // mixpanel cookie value with the same persistence_name - * // will be transferred to localStorage and deleted - * persistence: 'cookie' - * - * // name for super properties persistent store - * persistence_name: '' - * - * // names of properties/superproperties which should never - * // be sent with track() calls - * property_blacklist: [] - * - * // if this is true, mixpanel cookies will be marked as - * // secure, meaning they will only be transmitted over https - * secure_cookie: false - * - * // disables enriching user profiles with first touch marketing data - * skip_first_touch_marketing: false - * - * // the amount of time track_links will - * // wait for Mixpanel's servers to respond - * track_links_timeout: 300 - * - * // adds any UTM parameters and click IDs present on the page to any events fired - * track_marketing: true - * - * // enables automatic page view tracking using default page view events through - * // the track_pageview() method - * track_pageview: false - * - * // if you set upgrade to be true, the library will check for - * // a cookie from our old js library and import super - * // properties from it, then the old cookie is deleted - * // The upgrade config option only works in the initialization, - * // so make sure you set it when you create the library. - * upgrade: false - * - * // extra HTTP request headers to set for each API request, in - * // the format {'Header-Name': value} - * xhr_headers: {} - * - * // whether to ignore or respect the web browser's Do Not Track setting - * ignore_dnt: false - * } - * - * - * @param {Object} config A dictionary of new configuration values to update - */ -MixpanelLib.prototype.set_config = function(config) { - if (_.isObject(config)) { - _.extend(this['config'], config); - - var new_batch_size = config['batch_size']; - if (new_batch_size) { - _.each(this.request_batchers, function(batcher) { - batcher.resetBatchSize(); - }); - } - - if (!this.get_config('persistence_name')) { - this['config']['persistence_name'] = this['config']['cookie_name']; - } - if (!this.get_config('disable_persistence')) { - this['config']['disable_persistence'] = this['config']['disable_cookie']; - } - - if (this['persistence']) { - this['persistence'].update_config(this['config']); - } - Config.DEBUG = Config.DEBUG || this.get_config('debug'); - } -}; - -/** - * returns the current config object for the library. - */ -MixpanelLib.prototype.get_config = function(prop_name) { - return this['config'][prop_name]; -}; - -/** - * Fetch a hook function from config, with safe default, and run it - * against the given arguments - * @param {string} hook_name which hook to retrieve - * @returns {any|null} return value of user-provided hook, or null if nothing was returned - */ -MixpanelLib.prototype._run_hook = function(hook_name) { - var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1)); - if (typeof ret === 'undefined') { - this.report_error(hook_name + ' hook did not return a value'); - ret = null; - } - return ret; -}; - -/** - * Returns the value of the super property named property_name. If no such - * property is set, get_property() will return the undefined value. - * - * ### Notes: - * - * get_property() can only be called after the Mixpanel library has finished loading. - * init() has a loaded function available to handle this automatically. For example: - * - * // grab value for 'user_id' after the mixpanel library has loaded - * mixpanel.init('YOUR PROJECT TOKEN', { - * loaded: function(mixpanel) { - * user_id = mixpanel.get_property('user_id'); - * } - * }); - * - * @param {String} property_name The name of the super property you want to retrieve - */ -MixpanelLib.prototype.get_property = function(property_name) { - return this['persistence'].load_prop([property_name]); -}; - -MixpanelLib.prototype.toString = function() { - var name = this.get_config('name'); - if (name !== PRIMARY_INSTANCE_NAME) { - name = PRIMARY_INSTANCE_NAME + '.' + name; - } - return name; -}; - -MixpanelLib.prototype._event_is_disabled = function(event_name) { - return _.isBlockedUA(userAgent) || - this._flags.disable_all_events || - _.include(this.__disabled_events, event_name); -}; - -// perform some housekeeping around GDPR opt-in/out state -MixpanelLib.prototype._gdpr_init = function() { - var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; - - // try to convert opt-in/out cookies to localStorage if possible - if (is_localStorage_requested && _.localStorage.is_supported()) { - if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { - this.opt_in_tracking({'enable_persistence': false}); - } - if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { - this.opt_out_tracking({'clear_persistence': false}); - } - this.clear_opt_in_out_tracking({ - 'persistence_type': 'cookie', - 'enable_persistence': false - }); - } - - // check whether the user has already opted out - if so, clear & disable persistence - if (this.has_opted_out_tracking()) { - this._gdpr_update_persistence({'clear_persistence': true}); - - // check whether we should opt out by default - // note: we don't clear persistence here by default since opt-out default state is often - // used as an initial state while GDPR information is being collected - } else if (!this.has_opted_in_tracking() && ( - this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout') - )) { - _.cookie.remove('mp_optout'); - this.opt_out_tracking({ - 'clear_persistence': this.get_config('opt_out_persistence_by_default') - }); - } -}; - -/** - * Enable or disable persistence based on options - * only enable/disable if persistence is not already in this state - * @param {boolean} [options.clear_persistence] If true, will delete all data stored by the sdk in persistence and disable it - * @param {boolean} [options.enable_persistence] If true, will re-enable sdk persistence - */ -MixpanelLib.prototype._gdpr_update_persistence = function(options) { - var disabled; - if (options && options['clear_persistence']) { - disabled = true; - } else if (options && options['enable_persistence']) { - disabled = false; - } else { - return; - } - - if (!this.get_config('disable_persistence') && this['persistence'].disabled !== disabled) { - this['persistence'].set_disabled(disabled); - } - - if (disabled) { - this.stop_batch_senders(); - } else { - // only start batchers after opt-in if they have previously been started - // in order to avoid unintentionally starting up batching for the first time - if (this._batchers_were_started) { - this.start_batch_senders(); - } - } -}; - -// call a base gdpr function after constructing the appropriate token and options args -MixpanelLib.prototype._gdpr_call_func = function(func, options) { - options = _.extend({ - 'track': _.bind(this.track, this), - 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), - 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), - 'cookie_expiration': this.get_config('cookie_expiration'), - 'cross_site_cookie': this.get_config('cross_site_cookie'), - 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), - 'cookie_domain': this.get_config('cookie_domain'), - 'secure_cookie': this.get_config('secure_cookie'), - 'ignore_dnt': this.get_config('ignore_dnt') - }, options); - - // check if localStorage can be used for recording opt out status, fall back to cookie if not - if (!_.localStorage.is_supported()) { - options['persistence_type'] = 'cookie'; - } - - return func(this.get_config('token'), { - track: options['track'], - trackEventName: options['track_event_name'], - trackProperties: options['track_properties'], - persistenceType: options['persistence_type'], - persistencePrefix: options['cookie_prefix'], - cookieDomain: options['cookie_domain'], - cookieExpiration: options['cookie_expiration'], - crossSiteCookie: options['cross_site_cookie'], - crossSubdomainCookie: options['cross_subdomain_cookie'], - secureCookie: options['secure_cookie'], - ignoreDnt: options['ignore_dnt'] - }); -}; - -/** - * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // opt user in - * mixpanel.opt_in_tracking(); - * - * // opt user in with specific event name, properties, cookie configuration - * mixpanel.opt_in_tracking({ - * track_event_name: 'User opted in', - * track_event_properties: { - * 'Email': 'jdoe@example.com' - * }, - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) - * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action - * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action - * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.opt_in_tracking = function(options) { - options = _.extend({ - 'enable_persistence': true - }, options); - - this._gdpr_call_func(optIn, options); - this._gdpr_update_persistence(options); -}; - -/** - * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // opt user out - * mixpanel.opt_out_tracking(); - * - * // opt user out with different cookie configuration from Mixpanel instance - * mixpanel.opt_out_tracking({ - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out - * @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.opt_out_tracking = function(options) { - options = _.extend({ - 'clear_persistence': true, - 'delete_user': true - }, options); - - // delete user and clear charges since these methods may be disabled by opt-out - if (options['delete_user'] && this['people'] && this['people']._identify_called()) { - this['people'].delete_user(); - this['people'].clear_charges(); - } - - this._gdpr_call_func(optOut, options); - this._gdpr_update_persistence(options); -}; - -/** - * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * var has_opted_in = mixpanel.has_opted_in_tracking(); - * // use has_opted_in value - * - * @param {Object} [options] A dictionary of config options to override - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @returns {boolean} current opt-in status - */ -MixpanelLib.prototype.has_opted_in_tracking = function(options) { - return this._gdpr_call_func(hasOptedIn, options); -}; - -/** - * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * var has_opted_out = mixpanel.has_opted_out_tracking(); - * // use has_opted_out value - * - * @param {Object} [options] A dictionary of config options to override - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @returns {boolean} current opt-out status - */ -MixpanelLib.prototype.has_opted_out_tracking = function(options) { - return this._gdpr_call_func(hasOptedOut, options); -}; - -/** - * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance - * - * ### Usage: - * - * // clear user's opt-in/out status - * mixpanel.clear_opt_in_out_tracking(); - * - * // clear user's opt-in/out status with specific cookie configuration - should match - * // configuration used when opt_in_tracking/opt_out_tracking methods were called. - * mixpanel.clear_opt_in_out_tracking({ - * cookie_expiration: 30, - * secure_cookie: true - * }); - * - * @param {Object} [options] A dictionary of config options to override - * @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence - * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable - * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name - * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) - * @param {string} [options.cookie_domain] Custom cookie domain (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_site_cookie] Whether the opt-in cookie is set as cross-site-enabled (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) - * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) - */ -MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { - options = _.extend({ - 'enable_persistence': true - }, options); - - this._gdpr_call_func(clearOptInOut, options); - this._gdpr_update_persistence(options); -}; - -MixpanelLib.prototype.report_error = function(msg, err) { - console.error.apply(console.error, arguments); - try { - if (!err && !(msg instanceof Error)) { - msg = new Error(msg); - } - this.get_config('error_reporter')(msg, err); - } catch(err) { - console.error(err); - } -}; - -// EXPORTS (for closure compiler) - -// MixpanelLib Exports -MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; -MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; -MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; -MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; -MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; -MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; -MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; -MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; -MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; -MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; -MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; -MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; -MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; -MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; -MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; -MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; -MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; -MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; -MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; -MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; -MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; -MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; -MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; -MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; -MixpanelLib.prototype['get_group'] = MixpanelLib.prototype.get_group; -MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group; -MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group; -MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group; -MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups; -MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders; -MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders; -MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording; -MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording; -MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties; -MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES; - -// MixpanelPersistence Exports -MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; -MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; -MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; -MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; -MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; - - -var instances = {}; -var extend_mp = function() { - // add all the sub mixpanel instances - _.each(instances, function(instance, name) { - if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } - }); - - // add private functions as _ - mixpanel_master['_'] = _; -}; - -var override_mp_init_func = function() { - // we override the snippets init function to handle the case where a - // user initializes the mixpanel library after the script loads & runs - mixpanel_master['init'] = function(token, config, name) { - if (name) { - // initialize a sub library - if (!mixpanel_master[name]) { - mixpanel_master[name] = instances[name] = create_mplib(token, config, name); - mixpanel_master[name]._loaded(); - } - return mixpanel_master[name]; - } else { - var instance = mixpanel_master; - - if (instances[PRIMARY_INSTANCE_NAME]) { - // main mixpanel lib already initialized - instance = instances[PRIMARY_INSTANCE_NAME]; - } else if (token) { - // intialize the main mixpanel lib - instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); - instance._loaded(); - instances[PRIMARY_INSTANCE_NAME] = instance; - } - - mixpanel_master = instance; - if (init_type === INIT_SNIPPET) { - win[PRIMARY_INSTANCE_NAME] = mixpanel_master; - } - extend_mp(); - } - }; -}; - -var add_dom_loaded_handler = function() { - // Cross browser DOM Loaded support - function dom_loaded_handler() { - // function flag since we only want to execute this once - if (dom_loaded_handler.done) { return; } - dom_loaded_handler.done = true; - - DOM_LOADED = true; - ENQUEUE_REQUESTS = false; - - _.each(instances, function(inst) { - inst._dom_loaded(); - }); - } - - function do_scroll_check() { - try { - document$1.documentElement.doScroll('left'); - } catch(e) { - setTimeout(do_scroll_check, 1); - return; - } - - dom_loaded_handler(); - } - - if (document$1.addEventListener) { - if (document$1.readyState === 'complete') { - // safari 4 can fire the DOMContentLoaded event before loading all - // external JS (including this file). you will see some copypasta - // on the internet that checks for 'complete' and 'loaded', but - // 'loaded' is an IE thing - dom_loaded_handler(); - } else { - document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); - } - } else if (document$1.attachEvent) { - // IE - document$1.attachEvent('onreadystatechange', dom_loaded_handler); - - // check to make sure we arn't in a frame - var toplevel = false; - try { - toplevel = win.frameElement === null; - } catch(e) { - // noop - } - - if (document$1.documentElement.doScroll && toplevel) { - do_scroll_check(); - } - } - - // fallback handler, always will work - _.register_event(win, 'load', dom_loaded_handler, true); -}; - -function init_as_module(bundle_loader) { - load_extra_bundle = bundle_loader; - init_type = INIT_MODULE; - mixpanel_master = new MixpanelLib(); - - override_mp_init_func(); - mixpanel_master['init'](); - add_dom_loaded_handler(); - - return mixpanel_master; -} - -// For loading separate bundles asynchronously via script tag - -// For builds that do NOT want any extra bundles (e.g. session recorder) -// and just the main SDK, throw an error when trying to load a separate bundle. -// eslint-disable-next-line no-unused-vars -function loadThrowError (src, _onload) { - throw new Error('This build of Mixpanel only includes the main SDK, could not load ' + src); -} - -/* eslint camelcase: "off" */ - -var mixpanel = init_as_module(loadThrowError); - -module.exports = mixpanel; diff --git a/dist/mixpanel-recorder.js b/dist/mixpanel-recorder.js index 848cfd35..f16c0a14 100644 --- a/dist/mixpanel-recorder.js +++ b/dist/mixpanel-recorder.js @@ -4510,7 +4510,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; /* eslint camelcase: "off", eqeqeq: "off" */ diff --git a/dist/mixpanel-recorder.min.js b/dist/mixpanel-recorder.min.js index 593e5f5a..0479230a 100644 --- a/dist/mixpanel-recorder.min.js +++ b/dist/mixpanel-recorder.min.js @@ -29,7 +29,7 @@ or you can use record.mirror to access the mirror instance during recording.`;le LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */function e(c,u,f,m){function h(g){return g instanceof f?g:new f(function(p){p(g)})}return new(f||(f=Promise))(function(g,p){function y(v){try{S(m.next(v))}catch(b){p(b)}}function w(v){try{S(m.throw(v))}catch(b){p(b)}}function S(v){v.done?g(v.value):h(v.value).then(y,w)}S((m=m.apply(c,u||[])).next())})}for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r=typeof Uint8Array>"u"?[]:new Uint8Array(256),n=0;n>2],h+=t[(u[f]&3)<<4|u[f+1]>>4],h+=t[(u[f+1]&15)<<2|u[f+2]>>6],h+=t[u[f+2]&63];return m%3===2?h=h.substring(0,h.length-1)+"=":m%3===1&&(h=h.substring(0,h.length-2)+"=="),h};const o=new Map,a=new Map;function l(c,u,f){return e(this,void 0,void 0,function*(){const m=`${c}-${u}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const h=new OffscreenCanvas(c,u);h.getContext("2d");const p=yield(yield h.convertToBlob(f)).arrayBuffer(),y=i(p);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(c){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:u,bitmap:f,width:m,height:h,dataURLOptions:g}=c.data,p=l(m,h,g),y=new OffscreenCanvas(m,h);y.getContext("2d").drawImage(f,0,0),f.close();const S=yield y.convertToBlob(g),v=S.type,b=yield S.arrayBuffer(),I=i(b);if(!o.has(u)&&(yield p)===I)return o.set(u,I),s.postMessage({id:u});if(o.get(u)===I)return s.postMessage({id:u});s.postMessage({id:u,type:v,base64:I,width:m,height:h}),o.set(u,I)}else return s.postMessage({id:c.data.id})})}})()},null);class Tn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,c)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(c)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Zt(r,n,i,!0),l=new Map,s=new Rn;s.onmessage=g=>{const{id:p}=g.data;if(l.set(p,!1),!("base64"in g.data))return;const{base64:y,type:w,width:S,height:v}=g.data;this.mutationCb({id:p,type:ye["2D"],commands:[{property:"clearRect",args:[0,0,S,v]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const c=1e3/t;let u=0,f;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(p=>{U(p,n,i,!0)||g.push(p)}),g},h=g=>{if(u&&g-ubn(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(p);if(l.get(w)||p.width===0||p.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(p.__context)){const v=p.getContext(p.__context);((y=v?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&v.clear(v.COLOR_BUFFER_BIT)}const S=yield createImageBitmap(p);s.postMessage({id:w,bitmap:S,width:p.width,height:p.height,dataURLOptions:o.dataURLOptions},[S])})),f=requestAnimationFrame(h)};f=requestAnimationFrame(h),this.resetObservers=()=>{a(),cancelAnimationFrame(f)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Zt(t,r,n,!1),o=Cn(this.processMutation.bind(this),t,r,n),a=On(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>Sn(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class Dn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Kr,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:St(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class Nn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Le()})}let N,ze,lt,He=!1;const V=_r();function Oe(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:c=null,inlineStylesheet:u=!0,maskAllInputs:f,maskInputOptions:m,slimDOMOptions:h,maskInputFn:g,maskTextFn:p,hooks:y,packFn:w,sampling:S={},dataURLOptions:v={},mousemoveWait:b,recordDOM:I=!0,recordCanvas:B=!1,recordCrossOriginIframes:F=!1,recordAfter:x=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:R=!1,collectFonts:j=!1,inlineImages:G=!1,plugins:E,keepIframeSrcFn:le=()=>!1,ignoreCSSAttributes:z=new Set([]),errorHandler:ne}=e;tn(ne);const Z=F?window.parent===window:!0;let Qe=!1;if(!Z)try{window.parent.document&&(Qe=!1)}catch{Qe=!0}if(Z&&!t)throw new Error("emit function is required");b!==void 0&&S.mousemove===void 0&&(S.mousemove=b),V.reset();const pt=f===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},mt=h===!0||h==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:h==="all",headMetaDescKeywords:h==="all"}:h||{};Xr();let mr,gt=0;const gr=M=>{for(const K of E||[])K.eventProcessor&&(M=K.eventProcessor(M));return w&&!Qe&&(M=w(M)),M};N=(M,K)=>{var D;if(!((D=oe[0])===null||D===void 0)&&D.isFrozen()&&M.type!==O.FullSnapshot&&!(M.type===O.IncrementalSnapshot&&M.data.source===C.Mutation)&&oe.forEach(H=>H.unfreeze()),Z)t?.(gr(M),K);else if(Qe){const H={type:"rrweb",event:gr(M),origin:window.location.origin,isCheckout:K};window.parent.postMessage(H,"*")}if(M.type===O.FullSnapshot)mr=M,gt=0;else if(M.type===O.IncrementalSnapshot){if(M.data.source===C.Mutation&&M.data.isAttachIframe)return;gt++;const H=n&>>=n,de=r&&M.timestamp-mr.timestamp>r;(H||de)&&ze(!0)}};const Ze=M=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Mutation},M)}))},yr=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Scroll},M)})),vr=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},M)})),ei=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},M)})),ue=new Dn({mutationCb:Ze,adoptedStyleSheetCb:ei}),ce=new yn({mirror:V,mutationCb:Ze,stylesheetManager:ue,recordCrossOriginIframes:F,wrappedEmit:N});for(const M of E||[])M.getMirror&&M.getMirror({nodeMirror:V,crossOriginIframeMirror:ce.crossOriginIframeMirror,crossOriginIframeStyleMirror:ce.crossOriginIframeStyleMirror});const yt=new Nn;lt=new Tn({recordCanvas:B,mutationCb:vr,win:window,blockClass:i,blockSelector:o,mirror:V,sampling:S.canvas,dataURLOptions:v});const et=new vn({mutationCb:Ze,scrollCb:yr,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskInputOptions:pt,dataURLOptions:v,maskTextFn:p,maskInputFn:g,recordCanvas:B,inlineImages:G,sampling:S,slimDOMOptions:mt,iframeManager:ce,stylesheetManager:ue,canvasManager:lt,keepIframeSrcFn:le,processedNodeManager:yt},mirror:V});ze=(M=!1)=>{if(!I)return;N(L({type:O.Meta,data:{href:window.location.href,width:Tt(),height:Rt()}}),M),ue.reset(),et.init(),oe.forEach(D=>D.lock());const K=Vr(document,{mirror:V,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskAllInputs:pt,maskTextFn:p,slimDOM:mt,dataURLOptions:v,recordCanvas:B,inlineImages:G,onSerialize:D=>{At(D,V)&&ce.addIframe(D),Lt(D,V)&&ue.trackLinkElement(D),st(D)&&et.addShadowRoot(D.shadowRoot,document)},onIframeLoad:(D,H)=>{ce.attachIframe(D,H),et.observeAttachShadow(D)},onStylesheetLoad:(D,H)=>{ue.attachLinkElement(D,H)},keepIframeSrcFn:le});if(!K)return console.warn("Failed to snapshot the document");N(L({type:O.FullSnapshot,data:{node:K,initialOffset:xt(window)}}),M),oe.forEach(D=>D.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&ue.adoptStyleSheets(document.adoptedStyleSheets,V.getId(document))};try{const M=[],K=H=>{var de;return _(gn)({mutationCb:Ze,mousemoveCb:(T,vt)=>N(L({type:O.IncrementalSnapshot,data:{source:vt,positions:T}})),mouseInteractionCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},T)})),scrollCb:yr,viewportResizeCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},T)})),inputCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Input},T)})),mediaInteractionCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},T)})),styleSheetRuleCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},T)})),styleDeclarationCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},T)})),canvasMutationCb:vr,fontCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Font},T)})),selectionCb:T=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Selection},T)}))},customElementCb:T=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},T)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:c,maskInputOptions:pt,inlineStylesheet:u,sampling:S,recordDOM:I,recordCanvas:B,inlineImages:G,userTriggeredOnInput:R,collectFonts:j,doc:H,maskInputFn:g,maskTextFn:p,keepIframeSrcFn:le,blockSelector:o,slimDOMOptions:mt,dataURLOptions:v,mirror:V,iframeManager:ce,stylesheetManager:ue,shadowDomManager:et,processedNodeManager:yt,canvasManager:lt,ignoreCSSAttributes:z,plugins:((de=E?.filter(T=>T.observer))===null||de===void 0?void 0:de.map(T=>({observer:T.observer,options:T.options,callback:vt=>N(L({type:O.Plugin,data:{plugin:T.name,payload:vt}}))})))||[]},y)};ce.addLoadListener(H=>{try{M.push(K(H.contentDocument))}catch(de){console.warn(de)}});const D=()=>{ze(),M.push(K(document)),He=!0};return document.readyState==="interactive"||document.readyState==="complete"?D():(M.push(W("DOMContentLoaded",()=>{N(L({type:O.DomContentLoaded,data:{}})),x==="DOMContentLoaded"&&D()})),M.push(W("load",()=>{N(L({type:O.Load,data:{}})),x==="load"&&D()},window))),()=>{M.forEach(H=>H()),yt.destroy(),He=!1,rn()}}catch(M){console.warn(M)}}Oe.addCustomEvent=(e,t)=>{if(!He)throw new Error("please add custom event after start recording");N(L({type:O.Custom,data:{tag:e,payload:t}}))},Oe.freezePage=()=>{oe.forEach(e=>e.freeze())},Oe.takeFullSnapshot=e=>{if(!He)throw new Error("please take full snapshot after start recording");ze(e)},Oe.mirror=V;var tr=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(tr||{}),Y=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(Y||{}),rr={DEBUG:!1,LIB_VERSION:"2.54.0-rc1"},P;if(typeof window>"u"){var nr={hostname:""};P={navigator:{userAgent:""},document:{location:nr,referrer:""},screen:{width:0,height:0},location:nr}}else P=window;var $e=24*60*60*1e3,qe=Array.prototype,An=Function.prototype,ir=Object.prototype,se=qe.slice,Ee=ir.toString,je=ir.hasOwnProperty,ke=P.console,xe=P.navigator,q=P.document,Ge=P.opera,Ve=P.screen,ae=xe.userAgent,ut=An.bind,or=qe.forEach,sr=qe.indexOf,ar=qe.map,Ln=Array.isArray,ct={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},J={log:function(){},warn:function(){},error:function(){},critical:function(){if(!d.isUndefined(ke)&&ke){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{ke.error.apply(ke,e)}catch{d.each(e,function(r){ke.error(r)})}}}},dt=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(J,arguments)}},Je=function(e){return{log:dt(J.log,e),error:dt(J.error,e),critical:dt(J.critical,e)}};d.bind=function(e,t){var r,n;if(ut&&e.bind===ut)return ut.apply(e,se.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=se.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(se.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(se.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(or&&e.forEach===or)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,c="",u="",f=0,m=a,h=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,h=[],Ee.apply(g)==="[object Array]"){for(f=g.length,s=0;s"u"?[]:new Uint8Array(256),n=0;n>2],h+=t[(u[f]&3)<<4|u[f+1]>>4],h+=t[(u[f+1]&15)<<2|u[f+2]>>6],h+=t[u[f+2]&63];return m%3===2?h=h.substring(0,h.length-1)+"=":m%3===1&&(h=h.substring(0,h.length-2)+"=="),h};const o=new Map,a=new Map;function l(c,u,f){return e(this,void 0,void 0,function*(){const m=`${c}-${u}`;if("OffscreenCanvas"in globalThis){if(a.has(m))return a.get(m);const h=new OffscreenCanvas(c,u);h.getContext("2d");const p=yield(yield h.convertToBlob(f)).arrayBuffer(),y=i(p);return a.set(m,y),y}else return""})}const s=self;s.onmessage=function(c){return e(this,void 0,void 0,function*(){if("OffscreenCanvas"in globalThis){const{id:u,bitmap:f,width:m,height:h,dataURLOptions:g}=c.data,p=l(m,h,g),y=new OffscreenCanvas(m,h);y.getContext("2d").drawImage(f,0,0),f.close();const S=yield y.convertToBlob(g),v=S.type,b=yield S.arrayBuffer(),I=i(b);if(!o.has(u)&&(yield p)===I)return o.set(u,I),s.postMessage({id:u});if(o.get(u)===I)return s.postMessage({id:u});s.postMessage({id:u,type:v,base64:I,width:m,height:h}),o.set(u,I)}else return s.postMessage({id:c.data.id})})}})()},null);class Tn{reset(){this.pendingCanvasMutations.clear(),this.resetObservers&&this.resetObservers()}freeze(){this.frozen=!0}unfreeze(){this.frozen=!1}lock(){this.locked=!0}unlock(){this.locked=!1}constructor(t){this.pendingCanvasMutations=new Map,this.rafStamps={latestId:0,invokeId:null},this.frozen=!1,this.locked=!1,this.processMutation=(s,c)=>{(this.rafStamps.invokeId&&this.rafStamps.latestId!==this.rafStamps.invokeId||!this.rafStamps.invokeId)&&(this.rafStamps.invokeId=this.rafStamps.latestId),this.pendingCanvasMutations.has(s)||this.pendingCanvasMutations.set(s,[]),this.pendingCanvasMutations.get(s).push(c)};const{sampling:r="all",win:n,blockClass:i,blockSelector:o,recordCanvas:a,dataURLOptions:l}=t;this.mutationCb=t.mutationCb,this.mirror=t.mirror,a&&r==="all"&&this.initCanvasMutationObserver(n,i,o),a&&typeof r=="number"&&this.initCanvasFPSObserver(r,n,i,o,{dataURLOptions:l})}initCanvasFPSObserver(t,r,n,i,o){const a=Zt(r,n,i,!0),l=new Map,s=new Rn;s.onmessage=g=>{const{id:p}=g.data;if(l.set(p,!1),!("base64"in g.data))return;const{base64:y,type:w,width:S,height:v}=g.data;this.mutationCb({id:p,type:ye["2D"],commands:[{property:"clearRect",args:[0,0,S,v]},{property:"drawImage",args:[{rr_type:"ImageBitmap",args:[{rr_type:"Blob",data:[{rr_type:"ArrayBuffer",base64:y}],type:w}]},0,0]}]})};const c=1e3/t;let u=0,f;const m=()=>{const g=[];return r.document.querySelectorAll("canvas").forEach(p=>{U(p,n,i,!0)||g.push(p)}),g},h=g=>{if(u&&g-ubn(this,void 0,void 0,function*(){var y;const w=this.mirror.getId(p);if(l.get(w)||p.width===0||p.height===0)return;if(l.set(w,!0),["webgl","webgl2"].includes(p.__context)){const v=p.getContext(p.__context);((y=v?.getContextAttributes())===null||y===void 0?void 0:y.preserveDrawingBuffer)===!1&&v.clear(v.COLOR_BUFFER_BIT)}const S=yield createImageBitmap(p);s.postMessage({id:w,bitmap:S,width:p.width,height:p.height,dataURLOptions:o.dataURLOptions},[S])})),f=requestAnimationFrame(h)};f=requestAnimationFrame(h),this.resetObservers=()=>{a(),cancelAnimationFrame(f)}}initCanvasMutationObserver(t,r,n){this.startRAFTimestamping(),this.startPendingCanvasMutationFlusher();const i=Zt(t,r,n,!1),o=Cn(this.processMutation.bind(this),t,r,n),a=On(this.processMutation.bind(this),t,r,n,this.mirror);this.resetObservers=()=>{i(),o(),a()}}startPendingCanvasMutationFlusher(){requestAnimationFrame(()=>this.flushPendingCanvasMutations())}startRAFTimestamping(){const t=r=>{this.rafStamps.latestId=r,requestAnimationFrame(t)};requestAnimationFrame(t)}flushPendingCanvasMutations(){this.pendingCanvasMutations.forEach((t,r)=>{const n=this.mirror.getId(r);this.flushPendingCanvasMutationFor(r,n)}),requestAnimationFrame(()=>this.flushPendingCanvasMutations())}flushPendingCanvasMutationFor(t,r){if(this.frozen||this.locked)return;const n=this.pendingCanvasMutations.get(t);if(!n||r===-1)return;const i=n.map(a=>Sn(a,["type"])),{type:o}=n[0];this.mutationCb({id:r,type:o,commands:i}),this.pendingCanvasMutations.delete(t)}}class Dn{constructor(t){this.trackedLinkElements=new WeakSet,this.styleMirror=new Kr,this.mutationCb=t.mutationCb,this.adoptedStyleSheetCb=t.adoptedStyleSheetCb}attachLinkElement(t,r){"_cssText"in r.attributes&&this.mutationCb({adds:[],removes:[],texts:[],attributes:[{id:r.id,attributes:r.attributes}]}),this.trackLinkElement(t)}trackLinkElement(t){this.trackedLinkElements.has(t)||(this.trackedLinkElements.add(t),this.trackStylesheetInLinkElement(t))}adoptStyleSheets(t,r){if(t.length===0)return;const n={id:r,styleIds:[]},i=[];for(const o of t){let a;this.styleMirror.has(o)?a=this.styleMirror.getId(o):(a=this.styleMirror.add(o),i.push({styleId:a,rules:Array.from(o.rules||CSSRule,(l,s)=>({rule:St(l),index:s}))})),n.styleIds.push(a)}i.length>0&&(n.styles=i),this.adoptedStyleSheetCb(n)}reset(){this.styleMirror.reset(),this.trackedLinkElements=new WeakSet}trackStylesheetInLinkElement(t){}}class Nn{constructor(){this.nodeMap=new WeakMap,this.loop=!0,this.periodicallyClear()}periodicallyClear(){requestAnimationFrame(()=>{this.clear(),this.loop&&this.periodicallyClear()})}inOtherBuffer(t,r){const n=this.nodeMap.get(t);return n&&Array.from(n).some(i=>i!==r)}add(t,r){this.nodeMap.set(t,(this.nodeMap.get(t)||new Set).add(r))}clear(){this.nodeMap=new WeakMap}destroy(){this.loop=!1}}function L(e){return Object.assign(Object.assign({},e),{timestamp:Le()})}let N,ze,lt,He=!1;const V=_r();function Oe(e={}){const{emit:t,checkoutEveryNms:r,checkoutEveryNth:n,blockClass:i="rr-block",blockSelector:o=null,ignoreClass:a="rr-ignore",ignoreSelector:l=null,maskTextClass:s="rr-mask",maskTextSelector:c=null,inlineStylesheet:u=!0,maskAllInputs:f,maskInputOptions:m,slimDOMOptions:h,maskInputFn:g,maskTextFn:p,hooks:y,packFn:w,sampling:S={},dataURLOptions:v={},mousemoveWait:b,recordDOM:I=!0,recordCanvas:B=!1,recordCrossOriginIframes:F=!1,recordAfter:x=e.recordAfter==="DOMContentLoaded"?e.recordAfter:"load",userTriggeredOnInput:R=!1,collectFonts:j=!1,inlineImages:G=!1,plugins:E,keepIframeSrcFn:le=()=>!1,ignoreCSSAttributes:z=new Set([]),errorHandler:ne}=e;tn(ne);const Z=F?window.parent===window:!0;let Qe=!1;if(!Z)try{window.parent.document&&(Qe=!1)}catch{Qe=!0}if(Z&&!t)throw new Error("emit function is required");b!==void 0&&S.mousemove===void 0&&(S.mousemove=b),V.reset();const pt=f===!0?{color:!0,date:!0,"datetime-local":!0,email:!0,month:!0,number:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0,textarea:!0,select:!0,password:!0}:m!==void 0?m:{password:!0},mt=h===!0||h==="all"?{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaVerification:!0,headMetaAuthorship:h==="all",headMetaDescKeywords:h==="all"}:h||{};Xr();let mr,gt=0;const gr=M=>{for(const K of E||[])K.eventProcessor&&(M=K.eventProcessor(M));return w&&!Qe&&(M=w(M)),M};N=(M,K)=>{var D;if(!((D=oe[0])===null||D===void 0)&&D.isFrozen()&&M.type!==O.FullSnapshot&&!(M.type===O.IncrementalSnapshot&&M.data.source===C.Mutation)&&oe.forEach(H=>H.unfreeze()),Z)t?.(gr(M),K);else if(Qe){const H={type:"rrweb",event:gr(M),origin:window.location.origin,isCheckout:K};window.parent.postMessage(H,"*")}if(M.type===O.FullSnapshot)mr=M,gt=0;else if(M.type===O.IncrementalSnapshot){if(M.data.source===C.Mutation&&M.data.isAttachIframe)return;gt++;const H=n&>>=n,de=r&&M.timestamp-mr.timestamp>r;(H||de)&&ze(!0)}};const Ze=M=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Mutation},M)}))},yr=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Scroll},M)})),vr=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.CanvasMutation},M)})),ei=M=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.AdoptedStyleSheet},M)})),ue=new Dn({mutationCb:Ze,adoptedStyleSheetCb:ei}),ce=new yn({mirror:V,mutationCb:Ze,stylesheetManager:ue,recordCrossOriginIframes:F,wrappedEmit:N});for(const M of E||[])M.getMirror&&M.getMirror({nodeMirror:V,crossOriginIframeMirror:ce.crossOriginIframeMirror,crossOriginIframeStyleMirror:ce.crossOriginIframeStyleMirror});const yt=new Nn;lt=new Tn({recordCanvas:B,mutationCb:vr,win:window,blockClass:i,blockSelector:o,mirror:V,sampling:S.canvas,dataURLOptions:v});const et=new vn({mutationCb:Ze,scrollCb:yr,bypassOptions:{blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskInputOptions:pt,dataURLOptions:v,maskTextFn:p,maskInputFn:g,recordCanvas:B,inlineImages:G,sampling:S,slimDOMOptions:mt,iframeManager:ce,stylesheetManager:ue,canvasManager:lt,keepIframeSrcFn:le,processedNodeManager:yt},mirror:V});ze=(M=!1)=>{if(!I)return;N(L({type:O.Meta,data:{href:window.location.href,width:Tt(),height:Rt()}}),M),ue.reset(),et.init(),oe.forEach(D=>D.lock());const K=Vr(document,{mirror:V,blockClass:i,blockSelector:o,maskTextClass:s,maskTextSelector:c,inlineStylesheet:u,maskAllInputs:pt,maskTextFn:p,slimDOM:mt,dataURLOptions:v,recordCanvas:B,inlineImages:G,onSerialize:D=>{At(D,V)&&ce.addIframe(D),Lt(D,V)&&ue.trackLinkElement(D),st(D)&&et.addShadowRoot(D.shadowRoot,document)},onIframeLoad:(D,H)=>{ce.attachIframe(D,H),et.observeAttachShadow(D)},onStylesheetLoad:(D,H)=>{ue.attachLinkElement(D,H)},keepIframeSrcFn:le});if(!K)return console.warn("Failed to snapshot the document");N(L({type:O.FullSnapshot,data:{node:K,initialOffset:xt(window)}}),M),oe.forEach(D=>D.unlock()),document.adoptedStyleSheets&&document.adoptedStyleSheets.length>0&&ue.adoptStyleSheets(document.adoptedStyleSheets,V.getId(document))};try{const M=[],K=H=>{var de;return _(gn)({mutationCb:Ze,mousemoveCb:(T,vt)=>N(L({type:O.IncrementalSnapshot,data:{source:vt,positions:T}})),mouseInteractionCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.MouseInteraction},T)})),scrollCb:yr,viewportResizeCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.ViewportResize},T)})),inputCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Input},T)})),mediaInteractionCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.MediaInteraction},T)})),styleSheetRuleCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.StyleSheetRule},T)})),styleDeclarationCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.StyleDeclaration},T)})),canvasMutationCb:vr,fontCb:T=>N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Font},T)})),selectionCb:T=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.Selection},T)}))},customElementCb:T=>{N(L({type:O.IncrementalSnapshot,data:Object.assign({source:C.CustomElement},T)}))},blockClass:i,ignoreClass:a,ignoreSelector:l,maskTextClass:s,maskTextSelector:c,maskInputOptions:pt,inlineStylesheet:u,sampling:S,recordDOM:I,recordCanvas:B,inlineImages:G,userTriggeredOnInput:R,collectFonts:j,doc:H,maskInputFn:g,maskTextFn:p,keepIframeSrcFn:le,blockSelector:o,slimDOMOptions:mt,dataURLOptions:v,mirror:V,iframeManager:ce,stylesheetManager:ue,shadowDomManager:et,processedNodeManager:yt,canvasManager:lt,ignoreCSSAttributes:z,plugins:((de=E?.filter(T=>T.observer))===null||de===void 0?void 0:de.map(T=>({observer:T.observer,options:T.options,callback:vt=>N(L({type:O.Plugin,data:{plugin:T.name,payload:vt}}))})))||[]},y)};ce.addLoadListener(H=>{try{M.push(K(H.contentDocument))}catch(de){console.warn(de)}});const D=()=>{ze(),M.push(K(document)),He=!0};return document.readyState==="interactive"||document.readyState==="complete"?D():(M.push(W("DOMContentLoaded",()=>{N(L({type:O.DomContentLoaded,data:{}})),x==="DOMContentLoaded"&&D()})),M.push(W("load",()=>{N(L({type:O.Load,data:{}})),x==="load"&&D()},window))),()=>{M.forEach(H=>H()),yt.destroy(),He=!1,rn()}}catch(M){console.warn(M)}}Oe.addCustomEvent=(e,t)=>{if(!He)throw new Error("please add custom event after start recording");N(L({type:O.Custom,data:{tag:e,payload:t}}))},Oe.freezePage=()=>{oe.forEach(e=>e.freeze())},Oe.takeFullSnapshot=e=>{if(!He)throw new Error("please take full snapshot after start recording");ze(e)},Oe.mirror=V;var tr=(e=>(e[e.DomContentLoaded=0]="DomContentLoaded",e[e.Load=1]="Load",e[e.FullSnapshot=2]="FullSnapshot",e[e.IncrementalSnapshot=3]="IncrementalSnapshot",e[e.Meta=4]="Meta",e[e.Custom=5]="Custom",e[e.Plugin=6]="Plugin",e))(tr||{}),Y=(e=>(e[e.Mutation=0]="Mutation",e[e.MouseMove=1]="MouseMove",e[e.MouseInteraction=2]="MouseInteraction",e[e.Scroll=3]="Scroll",e[e.ViewportResize=4]="ViewportResize",e[e.Input=5]="Input",e[e.TouchMove=6]="TouchMove",e[e.MediaInteraction=7]="MediaInteraction",e[e.StyleSheetRule=8]="StyleSheetRule",e[e.CanvasMutation=9]="CanvasMutation",e[e.Font=10]="Font",e[e.Log=11]="Log",e[e.Drag=12]="Drag",e[e.StyleDeclaration=13]="StyleDeclaration",e[e.Selection=14]="Selection",e[e.AdoptedStyleSheet=15]="AdoptedStyleSheet",e[e.CustomElement=16]="CustomElement",e))(Y||{}),rr={DEBUG:!1,LIB_VERSION:"2.54.0"},P;if(typeof window>"u"){var nr={hostname:""};P={navigator:{userAgent:""},document:{location:nr,referrer:""},screen:{width:0,height:0},location:nr}}else P=window;var $e=24*60*60*1e3,qe=Array.prototype,An=Function.prototype,ir=Object.prototype,se=qe.slice,Ee=ir.toString,je=ir.hasOwnProperty,ke=P.console,xe=P.navigator,q=P.document,Ge=P.opera,Ve=P.screen,ae=xe.userAgent,ut=An.bind,or=qe.forEach,sr=qe.indexOf,ar=qe.map,Ln=Array.isArray,ct={},d={trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},J={log:function(){},warn:function(){},error:function(){},critical:function(){if(!d.isUndefined(ke)&&ke){var e=["Mixpanel error:"].concat(d.toArray(arguments));try{ke.error.apply(ke,e)}catch{d.each(e,function(r){ke.error(r)})}}}},dt=function(e,t){return function(){return arguments[0]="["+t+"] "+arguments[0],e.apply(J,arguments)}},Je=function(e){return{log:dt(J.log,e),error:dt(J.error,e),critical:dt(J.critical,e)}};d.bind=function(e,t){var r,n;if(ut&&e.bind===ut)return ut.apply(e,se.call(arguments,1));if(!d.isFunction(e))throw new TypeError;return r=se.call(arguments,2),n=function(){if(!(this instanceof n))return e.apply(t,r.concat(se.call(arguments)));var i={};i.prototype=e.prototype;var o=new i;i.prototype=null;var a=e.apply(o,r.concat(se.call(arguments)));return Object(a)===a?a:o},n},d.each=function(e,t,r){if(e!=null){if(or&&e.forEach===or)e.forEach(t,r);else if(e.length===+e.length){for(var n=0,i=e.length;n0&&(t[n]=r)}),t},d.truncate=function(e,t){var r;return typeof e=="string"?r=e.slice(0,t):d.isArray(e)?(r=[],d.each(e,function(n){r.push(d.truncate(n,t))})):d.isObject(e)?(r={},d.each(e,function(n,i){r[i]=d.truncate(n,t)})):r=e,r},d.JSONEncode=function(){return function(e){var t=e,r=function(i){var o=/[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,a={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return o.lastIndex=0,o.test(i)?'"'+i.replace(o,function(l){var s=a[l];return typeof s=="string"?s:"\\u"+("0000"+l.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+i+'"'},n=function(i,o){var a="",l=" ",s=0,c="",u="",f=0,m=a,h=[],g=o[i];switch(g&&typeof g=="object"&&typeof g.toJSON=="function"&&(g=g.toJSON(i)),typeof g){case"string":return r(g);case"number":return isFinite(g)?String(g):"null";case"boolean":case"null":return String(g);case"object":if(!g)return"null";if(a+=l,h=[],Ee.apply(g)==="[object Array]"){for(f=g.length,s=0;s { var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; /* eslint camelcase: "off", eqeqeq: "off" */ diff --git a/dist/mixpanel.globals.js b/dist/mixpanel.globals.js index 241a6f11..2ea62766 100644 --- a/dist/mixpanel.globals.js +++ b/dist/mixpanel.globals.js @@ -3,7 +3,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; /* eslint camelcase: "off", eqeqeq: "off" */ diff --git a/dist/mixpanel.min.js b/dist/mixpanel.min.js index 1570986e..8371ba98 100644 --- a/dist/mixpanel.min.js +++ b/dist/mixpanel.min.js @@ -39,7 +39,7 @@ ma:function(a,b,d){return d||c.i(a," OPR/")?c.i(a,"Mini")?"Opera Mini":"Opera":/ c.i(a,"Android")?"Android Mobile":c.i(a,"Konqueror")?"Konqueror":c.i(a,"Firefox")?"Firefox":c.i(a,"MSIE")||c.i(a,"Trident/")?"Internet Explorer":c.i(a,"Gecko")?"Mozilla":""},Ia:function(a,b,d){b={"Internet Explorer Mobile":/rv:(\d+(\.\d+)?)/,"Microsoft Edge":/Edge?\/(\d+(\.\d+)?)/,Chrome:/Chrome\/(\d+(\.\d+)?)/,"Chrome iOS":/CriOS\/(\d+(\.\d+)?)/,"UC Browser":/(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/,Safari:/Version\/(\d+(\.\d+)?)/,"Mobile Safari":/Version\/(\d+(\.\d+)?)/,Opera:/(Opera|OPR)\/(\d+(\.\d+)?)/, Firefox:/Firefox\/(\d+(\.\d+)?)/,"Firefox iOS":/FxiOS\/(\d+(\.\d+)?)/,Konqueror:/Konqueror:(\d+(\.\d+)?)/,BlackBerry:/BlackBerry (\d+(\.\d+)?)/,"Android Mobile":/android\s(\d+(\.\d+)?)/,"Samsung Internet":/SamsungBrowser\/(\d+(\.\d+)?)/,"Internet Explorer":/(rv:|MSIE )(\d+(\.\d+)?)/,Mozilla:/rv:(\d+(\.\d+)?)/}[c.info.ma(a,b,d)];if(b===l)return r;a=a.match(b);return!a?r:parseFloat(a[a.length-2])},Pb:function(){return/Windows/i.test(z)?/Phone/.test(z)||/WPDesktop/.test(z)?"Windows Phone":"Windows": /(iPhone|iPad|iPod)/.test(z)?"iOS":/Android/.test(z)?"Android":/(BlackBerry|PlayBook|BB10)/i.test(z)?"BlackBerry":/Mac/i.test(z)?"Mac OS X":/Linux/.test(z)?"Linux":/CrOS/.test(z)?"Chrome OS":""},Cb:function(a){return/Windows Phone/i.test(a)||/WPDesktop/.test(a)?"Windows Phone":/iPad/.test(a)?"iPad":/iPod/.test(a)?"iPod Touch":/iPhone/.test(a)?"iPhone":/(BlackBerry|PlayBook|BB10)/i.test(a)?"BlackBerry":/Android/.test(a)?"Android":""},Ub:function(a){a=a.split("/");return 3<=a.length?a[2]:""},La:function(){return o.location.href}, -ba:function(a){"object"!==typeof a&&(a={});return c.extend(c.fa({$os:c.info.Pb(),$browser:c.info.ma(z,I.vendor,Y),$referrer:v.referrer,$referring_domain:c.info.Ub(v.referrer),$device:c.info.Cb(z)}),{$current_url:c.info.La(),$browser_version:c.info.Ia(z,I.vendor,Y),$screen_height:Z.height,$screen_width:Z.width,mp_lib:"web",$lib_version:"2.54.0-rc1",$insert_id:ea(),time:c.timestamp()/1E3},c.fa(a))},Vc:function(){return c.extend(c.fa({$os:c.info.Pb(),$browser:c.info.ma(z,I.vendor,Y)}),{$browser_version:c.info.Ia(z, +ba:function(a){"object"!==typeof a&&(a={});return c.extend(c.fa({$os:c.info.Pb(),$browser:c.info.ma(z,I.vendor,Y),$referrer:v.referrer,$referring_domain:c.info.Ub(v.referrer),$device:c.info.Cb(z)}),{$current_url:c.info.La(),$browser_version:c.info.Ia(z,I.vendor,Y),$screen_height:Z.height,$screen_width:Z.width,mp_lib:"web",$lib_version:"2.54.0",$insert_id:ea(),time:c.timestamp()/1E3},c.fa(a))},Vc:function(){return c.extend(c.fa({$os:c.info.Pb(),$browser:c.info.ma(z,I.vendor,Y)}),{$browser_version:c.info.Ia(z, I.vendor,Y)})},Sc:function(){return c.fa({current_page_title:v.title,current_domain:o.location.hostname,current_url_path:o.location.pathname,current_url_protocol:o.location.protocol,current_url_search:o.location.search})}};var Ha=/[a-z0-9][a-z0-9-]*\.[a-z]+$/i,Ga=/[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i,$=r,aa=r;if("undefined"!==typeof JSON)$=JSON.stringify,aa=JSON.parse;$=$||c.ha;aa=aa||c.T;c.toArray=c.Q;c.isObject=c.e;c.JSONEncode=c.ha;c.JSONDecode=c.T;c.isBlockedUA=c.Kb;c.isEmptyObject=c.ra;c.info= c.info;c.info.device=c.info.Cb;c.info.browser=c.info.ma;c.info.browserVersion=c.info.Ia;c.info.properties=c.info.ba;E.prototype.oa=function(){};E.prototype.Oa=function(){};E.prototype.Ga=function(){};E.prototype.Va=function(a){this.Mb=a;return this};E.prototype.o=function(a,b,d,f){var h=this,g=c.Fc(a);if(0===g.length)n.error("The DOM query ("+a+") returned 0 elements");else return c.a(g,function(a){c.Vb(a,this.Qb,function(a){var c={},g=h.oa(d,this),e=h.Mb.c("track_links_timeout");h.Oa(a,this,c);window.setTimeout(h.jc(f, g,c,m),e);h.Mb.o(b,g,h.jc(f,g,c))})},this),m};E.prototype.jc=function(a,b,c,f){var f=f||D,h=this;return function(){if(!c.Cc)c.Cc=m,a&&a(f,b)===D||h.Ga(b,c,f)}};E.prototype.oa=function(a,b){return"function"===typeof a?a(b):c.extend({},a)};c.Jb(M,E);M.prototype.oa=function(a,b){var c=M.pd.oa.apply(this,arguments);if(b.href)c.url=b.href;return c};M.prototype.Oa=function(a,b,c){c.Nb=2===a.which||a.metaKey||a.ctrlKey||"_blank"===b.target;c.href=b.href;c.Nb||a.preventDefault()};M.prototype.Ga=function(a, @@ -50,7 +50,7 @@ a);d&&d(D)},this),this.ua):(this.D.push(f),d&&d(m))};G.prototype.Hc=function(a){ this.ea(),f=0;ft.length)this.N();else{this.Xb=m;var k=c.bind(function(k){this.Xb=D;try{var t=D;if(a.lc)this.ca.yd(i);else if(c.e(k)&&"timeout"===k.error&&(new Date).getTime()-d>=b)this.h("Network timeout; retrying"),this.flush();else if(c.e(k)&&(500<=k.Sa||429===k.Sa||"timeout"===k.error)){var j=2*this.pa;k.Zb&&(j=1E3*parseInt(k.Zb,10)||j);j=Math.min(6E5,j);this.h("Error; retry in "+j+" ms");this.$b(j)}else if(c.e(k)&& 413===k.Sa)if(1 { var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; /* eslint camelcase: "off", eqeqeq: "off" */ diff --git a/examples/es2015-babelify/bundle.js b/examples/es2015-babelify/bundle.js index e0aca8ba..2e398763 100644 --- a/examples/es2015-babelify/bundle.js +++ b/examples/es2015-babelify/bundle.js @@ -10982,7 +10982,7 @@ Object.defineProperty(exports, '__esModule', { }); var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; exports['default'] = Config; diff --git a/examples/umd-webpack/bundle.js b/examples/umd-webpack/bundle.js index a56feacf..24111e0b 100644 --- a/examples/umd-webpack/bundle.js +++ b/examples/umd-webpack/bundle.js @@ -4576,7 +4576,7 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; /* eslint camelcase: "off", eqeqeq: "off" */ diff --git a/src/config.js b/src/config.js index 81500b1d..5a048ba7 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,6 @@ var Config = { DEBUG: false, - LIB_VERSION: '2.54.0-rc1' + LIB_VERSION: '2.54.0' }; export default Config; From 7f558a03446e9bf8732bb6addf89a68ed9932e71 Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 23 Jul 2024 21:43:03 +0000 Subject: [PATCH 47/48] changelog for 2.54.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f3f6d9..cbb5cc5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +**2.54.0** (23 Jul 2024) +- Provides optional builds without session recording module and without asynchronous script loading. +- Integrates request batcher with session recording module for increased reliability. +- Improved user inactivity heuristic for session recording timeout. +- Adds config options to inline images and collect fonts during session recording. + **2.53.0** (21 Jun 2024) - Switch to new session-recording network payload format, utilizing client-side compression when available - Session-recording methods are now available through Google Tag Manager wrapper From e1b1a7c93be04f909db887c0c57d699774028dac Mon Sep 17 00:00:00 2001 From: Jakub Grzegorzewski Date: Tue, 23 Jul 2024 21:43:30 +0000 Subject: [PATCH 48/48] 2.54.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6426011..2cca95d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mixpanel-browser", - "version": "2.53.0", + "version": "2.54.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mixpanel-browser", - "version": "2.53.0", + "version": "2.54.0", "license": "Apache-2.0", "dependencies": { "rrweb": "2.0.0-alpha.13" diff --git a/package.json b/package.json index 04ccb6dd..f83b8646 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mixpanel-browser", - "version": "2.53.0", + "version": "2.54.0", "description": "The official Mixpanel JavaScript browser client library", "main": "dist/mixpanel.cjs.js", "directories": {