mixpanel-browser 2.55.1 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +31 -1
- package/CHANGELOG.md +8 -0
- package/dist/mixpanel-core.cjs.js +32 -10
- package/dist/mixpanel-recorder.js +162 -74
- package/dist/mixpanel-recorder.min.js +8 -7
- package/dist/mixpanel-recorder.min.js.map +1 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +32 -10
- package/dist/mixpanel.amd.js +188 -80
- package/dist/mixpanel.cjs.js +188 -80
- package/dist/mixpanel.globals.js +32 -10
- package/dist/mixpanel.min.js +106 -105
- package/dist/mixpanel.min.js.map +8 -0
- package/dist/mixpanel.module.js +188 -80
- package/dist/mixpanel.umd.js +188 -80
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +26 -6
- package/src/recorder/index.js +39 -252
- package/src/recorder/rollup.config.js +2 -1
- package/src/recorder/session-recording.js +305 -0
- package/src/request-queue.js +5 -3
package/.eslintrc.json
CHANGED
|
@@ -27,5 +27,35 @@
|
|
|
27
27
|
"error",
|
|
28
28
|
"always"
|
|
29
29
|
]
|
|
30
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"overrides": [{
|
|
32
|
+
"files": ["tests/unit/**/*.js"],
|
|
33
|
+
"parserOptions": {
|
|
34
|
+
"ecmaVersion": 8,
|
|
35
|
+
"sourceType": "module"
|
|
36
|
+
},
|
|
37
|
+
"env": {
|
|
38
|
+
"mocha": true
|
|
39
|
+
},
|
|
40
|
+
"rules": {
|
|
41
|
+
"camelcase": "error",
|
|
42
|
+
"eol-last": "error",
|
|
43
|
+
"eqeqeq": "error",
|
|
44
|
+
"indent": ["error", 2],
|
|
45
|
+
"linebreak-style": [
|
|
46
|
+
"error",
|
|
47
|
+
"unix"
|
|
48
|
+
],
|
|
49
|
+
"no-console": "off",
|
|
50
|
+
"no-trailing-spaces": "error",
|
|
51
|
+
"quotes": [
|
|
52
|
+
"error",
|
|
53
|
+
"backtick"
|
|
54
|
+
],
|
|
55
|
+
"semi": [
|
|
56
|
+
"error",
|
|
57
|
+
"always"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}]
|
|
31
61
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
**2.56.0** (4 Nov 2024)
|
|
2
|
+
- Recording payloads now include additional metadata: the current URL, library type, and library version.
|
|
3
|
+
- Sourcemaps are now generated for the recorder module.
|
|
4
|
+
- Added debugging method `mixpanel.get_session_replay_url()` which will return a Mixpanel UI link to the session replay if there is an active recording taking place.
|
|
5
|
+
- Refactored session recording module to encapsulate each active recording and its metadata. Added a unit test suite for the new `session-recording.js`.
|
|
6
|
+
- Added some additional error handling for when `stopRecording` fails or rrweb silently fails to start recording.
|
|
7
|
+
- Removed `record_inline_images` option due to buggy behavior in rrweb.
|
|
8
|
+
|
|
1
9
|
**2.55.1** (27 Aug 2024)
|
|
2
10
|
- Adds a minimum recording length option for session recording
|
|
3
11
|
- Fixes and improvements for session recording batcher to support offline queueing and retry
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var Config = {
|
|
4
4
|
DEBUG: false,
|
|
5
|
-
LIB_VERSION: '2.
|
|
5
|
+
LIB_VERSION: '2.56.0'
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
@@ -2056,11 +2056,13 @@ var logger$1 = console_with_prefix('batch');
|
|
|
2056
2056
|
var RequestQueue = function(storageKey, options) {
|
|
2057
2057
|
options = options || {};
|
|
2058
2058
|
this.storageKey = storageKey;
|
|
2059
|
-
this.
|
|
2059
|
+
this.usePersistence = options.usePersistence;
|
|
2060
|
+
if (this.usePersistence) {
|
|
2061
|
+
this.storage = options.storage || window.localStorage;
|
|
2062
|
+
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
2063
|
+
}
|
|
2060
2064
|
this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
|
|
2061
|
-
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
2062
2065
|
|
|
2063
|
-
this.usePersistence = options.usePersistence;
|
|
2064
2066
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
2065
2067
|
|
|
2066
2068
|
this.memQueue = [];
|
|
@@ -4257,7 +4259,6 @@ var DEFAULT_CONFIG = {
|
|
|
4257
4259
|
'record_block_selector': 'img, video',
|
|
4258
4260
|
'record_collect_fonts': false,
|
|
4259
4261
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
4260
|
-
'record_inline_images': false,
|
|
4261
4262
|
'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
|
|
4262
4263
|
'record_mask_text_selector': '*',
|
|
4263
4264
|
'record_max_ms': MAX_RECORDING_MS,
|
|
@@ -4510,15 +4511,35 @@ MixpanelLib.prototype.stop_session_recording = function () {
|
|
|
4510
4511
|
|
|
4511
4512
|
MixpanelLib.prototype.get_session_recording_properties = function () {
|
|
4512
4513
|
var props = {};
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
props['$mp_replay_id'] = replay_id;
|
|
4517
|
-
}
|
|
4514
|
+
var replay_id = this._get_session_replay_id();
|
|
4515
|
+
if (replay_id) {
|
|
4516
|
+
props['$mp_replay_id'] = replay_id;
|
|
4518
4517
|
}
|
|
4519
4518
|
return props;
|
|
4520
4519
|
};
|
|
4521
4520
|
|
|
4521
|
+
MixpanelLib.prototype.get_session_replay_url = function () {
|
|
4522
|
+
var replay_url = null;
|
|
4523
|
+
var replay_id = this._get_session_replay_id();
|
|
4524
|
+
if (replay_id) {
|
|
4525
|
+
var query_params = _.HTTPBuildQuery({
|
|
4526
|
+
'replay_id': replay_id,
|
|
4527
|
+
'distinct_id': this.get_distinct_id(),
|
|
4528
|
+
'token': this.get_config('token')
|
|
4529
|
+
});
|
|
4530
|
+
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
4531
|
+
}
|
|
4532
|
+
return replay_url;
|
|
4533
|
+
};
|
|
4534
|
+
|
|
4535
|
+
MixpanelLib.prototype._get_session_replay_id = function () {
|
|
4536
|
+
var replay_id = null;
|
|
4537
|
+
if (this._recorder) {
|
|
4538
|
+
replay_id = this._recorder['replayId'];
|
|
4539
|
+
}
|
|
4540
|
+
return replay_id || null;
|
|
4541
|
+
};
|
|
4542
|
+
|
|
4522
4543
|
// Private methods
|
|
4523
4544
|
|
|
4524
4545
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -6245,6 +6266,7 @@ MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.protot
|
|
|
6245
6266
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
6246
6267
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
6247
6268
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
6269
|
+
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
6248
6270
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
6249
6271
|
|
|
6250
6272
|
// MixpanelPersistence Exports
|
|
@@ -4510,7 +4510,7 @@
|
|
|
4510
4510
|
|
|
4511
4511
|
var Config = {
|
|
4512
4512
|
DEBUG: false,
|
|
4513
|
-
LIB_VERSION: '2.
|
|
4513
|
+
LIB_VERSION: '2.56.0'
|
|
4514
4514
|
};
|
|
4515
4515
|
|
|
4516
4516
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
@@ -6386,7 +6386,7 @@
|
|
|
6386
6386
|
};
|
|
6387
6387
|
}
|
|
6388
6388
|
|
|
6389
|
-
var logger$
|
|
6389
|
+
var logger$4 = console_with_prefix('lock');
|
|
6390
6390
|
|
|
6391
6391
|
/**
|
|
6392
6392
|
* SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
|
|
@@ -6443,7 +6443,7 @@
|
|
|
6443
6443
|
|
|
6444
6444
|
var delay = function(cb) {
|
|
6445
6445
|
if (new Date().getTime() - startTime > timeoutMS) {
|
|
6446
|
-
logger$
|
|
6446
|
+
logger$4.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
|
|
6447
6447
|
storage.removeItem(keyZ);
|
|
6448
6448
|
storage.removeItem(keyY);
|
|
6449
6449
|
loop();
|
|
@@ -6532,7 +6532,7 @@
|
|
|
6532
6532
|
}
|
|
6533
6533
|
};
|
|
6534
6534
|
|
|
6535
|
-
var logger$
|
|
6535
|
+
var logger$3 = console_with_prefix('batch');
|
|
6536
6536
|
|
|
6537
6537
|
/**
|
|
6538
6538
|
* RequestQueue: queue for batching API requests with localStorage backup for retries.
|
|
@@ -6553,11 +6553,13 @@
|
|
|
6553
6553
|
var RequestQueue = function(storageKey, options) {
|
|
6554
6554
|
options = options || {};
|
|
6555
6555
|
this.storageKey = storageKey;
|
|
6556
|
-
this.storage = options.storage || window.localStorage;
|
|
6557
|
-
this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2);
|
|
6558
|
-
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6559
|
-
|
|
6560
6556
|
this.usePersistence = options.usePersistence;
|
|
6557
|
+
if (this.usePersistence) {
|
|
6558
|
+
this.storage = options.storage || window.localStorage;
|
|
6559
|
+
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
6560
|
+
}
|
|
6561
|
+
this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
|
|
6562
|
+
|
|
6561
6563
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
6562
6564
|
|
|
6563
6565
|
this.memQueue = [];
|
|
@@ -6836,7 +6838,7 @@
|
|
|
6836
6838
|
// maximum interval between request retries after exponential backoff
|
|
6837
6839
|
var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
6838
6840
|
|
|
6839
|
-
var logger$
|
|
6841
|
+
var logger$2 = console_with_prefix('batch');
|
|
6840
6842
|
|
|
6841
6843
|
/**
|
|
6842
6844
|
* RequestBatcher: manages the queueing, flushing, retry etc of requests of one
|
|
@@ -6950,7 +6952,7 @@
|
|
|
6950
6952
|
try {
|
|
6951
6953
|
|
|
6952
6954
|
if (this.requestInProgress) {
|
|
6953
|
-
logger$
|
|
6955
|
+
logger$2.log('Flush: Request already in progress');
|
|
6954
6956
|
return;
|
|
6955
6957
|
}
|
|
6956
6958
|
|
|
@@ -7118,7 +7120,7 @@
|
|
|
7118
7120
|
if (options.unloading) {
|
|
7119
7121
|
requestOptions.transport = 'sendBeacon';
|
|
7120
7122
|
}
|
|
7121
|
-
logger$
|
|
7123
|
+
logger$2.log('MIXPANEL REQUEST:', dataForRequest);
|
|
7122
7124
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
7123
7125
|
} catch(err) {
|
|
7124
7126
|
this.reportError('Error flushing request queue', err);
|
|
@@ -7130,7 +7132,7 @@
|
|
|
7130
7132
|
* Log error to global logger and optional user-defined logger.
|
|
7131
7133
|
*/
|
|
7132
7134
|
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
7133
|
-
logger$
|
|
7135
|
+
logger$2.error.apply(logger$2.error, arguments);
|
|
7134
7136
|
if (this.errorReporter) {
|
|
7135
7137
|
try {
|
|
7136
7138
|
if (!(err instanceof Error)) {
|
|
@@ -7138,12 +7140,12 @@
|
|
|
7138
7140
|
}
|
|
7139
7141
|
this.errorReporter(msg, err);
|
|
7140
7142
|
} catch(err) {
|
|
7141
|
-
logger$
|
|
7143
|
+
logger$2.error(err);
|
|
7142
7144
|
}
|
|
7143
7145
|
}
|
|
7144
7146
|
};
|
|
7145
7147
|
|
|
7146
|
-
var logger = console_with_prefix('recorder');
|
|
7148
|
+
var logger$1 = console_with_prefix('recorder');
|
|
7147
7149
|
var CompressionStream = win['CompressionStream'];
|
|
7148
7150
|
|
|
7149
7151
|
var RECORDER_BATCHER_LIB_CONFIG = {
|
|
@@ -7169,65 +7171,78 @@
|
|
|
7169
7171
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7170
7172
|
}
|
|
7171
7173
|
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
+
/**
|
|
7175
|
+
* This class encapsulates a single session recording and its lifecycle.
|
|
7176
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7177
|
+
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7178
|
+
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7179
|
+
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7180
|
+
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
7181
|
+
*/
|
|
7182
|
+
var SessionRecording = function(options) {
|
|
7183
|
+
this._mixpanel = options.mixpanelInstance;
|
|
7184
|
+
this._onIdleTimeout = options.onIdleTimeout;
|
|
7185
|
+
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7186
|
+
this._rrwebRecord = options.rrwebRecord;
|
|
7187
|
+
|
|
7188
|
+
this.replayId = options.replayId;
|
|
7174
7189
|
|
|
7175
7190
|
// internal rrweb stopRecording function
|
|
7176
7191
|
this._stopRecording = null;
|
|
7177
7192
|
|
|
7178
|
-
this.recEvents = [];
|
|
7179
7193
|
this.seqNo = 0;
|
|
7180
|
-
this.replayId = null;
|
|
7181
7194
|
this.replayStartTime = null;
|
|
7182
|
-
this.
|
|
7195
|
+
this.batchStartUrl = null;
|
|
7183
7196
|
|
|
7184
7197
|
this.idleTimeoutId = null;
|
|
7185
7198
|
this.maxTimeoutId = null;
|
|
7186
7199
|
|
|
7187
7200
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7188
7201
|
this.recordMinMs = 0;
|
|
7189
|
-
this._initBatcher();
|
|
7190
|
-
};
|
|
7191
|
-
|
|
7192
7202
|
|
|
7193
|
-
|
|
7194
|
-
this
|
|
7195
|
-
|
|
7196
|
-
|
|
7203
|
+
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7204
|
+
// this will be important when persistence is introduced
|
|
7205
|
+
var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
|
|
7206
|
+
this.batcher = new RequestBatcher(batcherKey, {
|
|
7197
7207
|
errorReporter: _.bind(this.reportError, this),
|
|
7198
7208
|
flushOnlyOnInterval: true,
|
|
7209
|
+
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7210
|
+
sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
|
|
7199
7211
|
usePersistence: false
|
|
7200
7212
|
});
|
|
7201
7213
|
};
|
|
7202
7214
|
|
|
7203
|
-
|
|
7204
|
-
MixpanelRecorder.prototype.get_config = function(configVar) {
|
|
7215
|
+
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7205
7216
|
return this._mixpanel.get_config(configVar);
|
|
7206
7217
|
};
|
|
7207
7218
|
|
|
7208
|
-
|
|
7219
|
+
// Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
|
|
7220
|
+
// reaches into this class instance and expects the snake case version of the function.
|
|
7221
|
+
// eslint-disable-next-line camelcase
|
|
7222
|
+
SessionRecording.prototype.get_config = function(configVar) {
|
|
7223
|
+
return this.getConfig(configVar);
|
|
7224
|
+
};
|
|
7225
|
+
|
|
7226
|
+
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
7209
7227
|
if (this._stopRecording !== null) {
|
|
7210
|
-
logger.log('Recording already in progress, skipping startRecording.');
|
|
7228
|
+
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
7211
7229
|
return;
|
|
7212
7230
|
}
|
|
7213
7231
|
|
|
7214
|
-
this.recordMaxMs = this.
|
|
7232
|
+
this.recordMaxMs = this.getConfig('record_max_ms');
|
|
7215
7233
|
if (this.recordMaxMs > MAX_RECORDING_MS) {
|
|
7216
7234
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7217
|
-
logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7235
|
+
logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7218
7236
|
}
|
|
7219
7237
|
|
|
7220
|
-
this.recordMinMs = this.
|
|
7238
|
+
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7221
7239
|
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7222
7240
|
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7223
|
-
logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7241
|
+
logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7224
7242
|
}
|
|
7225
7243
|
|
|
7226
|
-
this.recEvents = [];
|
|
7227
|
-
this.seqNo = 0;
|
|
7228
7244
|
this.replayStartTime = new Date().getTime();
|
|
7229
|
-
|
|
7230
|
-
this.replayId = _.UUID();
|
|
7245
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7231
7246
|
|
|
7232
7247
|
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7233
7248
|
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
@@ -7242,18 +7257,15 @@
|
|
|
7242
7257
|
|
|
7243
7258
|
var resetIdleTimeout = _.bind(function () {
|
|
7244
7259
|
clearTimeout(this.idleTimeoutId);
|
|
7245
|
-
this.idleTimeoutId = setTimeout(
|
|
7246
|
-
logger.log('Idle timeout reached, restarting recording.');
|
|
7247
|
-
this.resetRecording();
|
|
7248
|
-
}, this), this.get_config('record_idle_timeout_ms'));
|
|
7260
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
|
|
7249
7261
|
}, this);
|
|
7250
7262
|
|
|
7251
|
-
var blockSelector = this.
|
|
7263
|
+
var blockSelector = this.getConfig('record_block_selector');
|
|
7252
7264
|
if (blockSelector === '' || blockSelector === null) {
|
|
7253
7265
|
blockSelector = undefined;
|
|
7254
7266
|
}
|
|
7255
7267
|
|
|
7256
|
-
this._stopRecording =
|
|
7268
|
+
this._stopRecording = this._rrwebRecord({
|
|
7257
7269
|
'emit': _.bind(function (ev) {
|
|
7258
7270
|
this.batcher.enqueue(ev);
|
|
7259
7271
|
if (isUserEvent(ev)) {
|
|
@@ -7264,28 +7276,33 @@
|
|
|
7264
7276
|
resetIdleTimeout();
|
|
7265
7277
|
}
|
|
7266
7278
|
}, this),
|
|
7267
|
-
'blockClass': this.
|
|
7279
|
+
'blockClass': this.getConfig('record_block_class'),
|
|
7268
7280
|
'blockSelector': blockSelector,
|
|
7269
|
-
'collectFonts': this.
|
|
7270
|
-
'inlineImages': this.get_config('record_inline_images'),
|
|
7281
|
+
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
7271
7282
|
'maskAllInputs': true,
|
|
7272
|
-
'maskTextClass': this.
|
|
7273
|
-
'maskTextSelector': this.
|
|
7283
|
+
'maskTextClass': this.getConfig('record_mask_text_class'),
|
|
7284
|
+
'maskTextSelector': this.getConfig('record_mask_text_selector')
|
|
7274
7285
|
});
|
|
7275
7286
|
|
|
7276
|
-
|
|
7287
|
+
if (typeof this._stopRecording !== 'function') {
|
|
7288
|
+
this.reportError('rrweb failed to start, skipping this recording.');
|
|
7289
|
+
this._stopRecording = null;
|
|
7290
|
+
this.stopRecording(); // stop batcher looping and any timeouts
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7277
7293
|
|
|
7278
|
-
|
|
7279
|
-
};
|
|
7294
|
+
resetIdleTimeout();
|
|
7280
7295
|
|
|
7281
|
-
|
|
7282
|
-
this.stopRecording();
|
|
7283
|
-
this.startRecording(true);
|
|
7296
|
+
this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
|
|
7284
7297
|
};
|
|
7285
7298
|
|
|
7286
|
-
|
|
7287
|
-
if (this.
|
|
7288
|
-
|
|
7299
|
+
SessionRecording.prototype.stopRecording = function () {
|
|
7300
|
+
if (!this.isRrwebStopped()) {
|
|
7301
|
+
try {
|
|
7302
|
+
this._stopRecording();
|
|
7303
|
+
} catch (err) {
|
|
7304
|
+
this.reportError('Error with rrweb stopRecording', err);
|
|
7305
|
+
}
|
|
7289
7306
|
this._stopRecording = null;
|
|
7290
7307
|
}
|
|
7291
7308
|
|
|
@@ -7297,35 +7314,38 @@
|
|
|
7297
7314
|
this.batcher.flush();
|
|
7298
7315
|
this.batcher.stop();
|
|
7299
7316
|
}
|
|
7300
|
-
this.replayId = null;
|
|
7301
7317
|
|
|
7302
7318
|
clearTimeout(this.idleTimeoutId);
|
|
7303
7319
|
clearTimeout(this.maxTimeoutId);
|
|
7304
7320
|
};
|
|
7305
7321
|
|
|
7322
|
+
SessionRecording.prototype.isRrwebStopped = function () {
|
|
7323
|
+
return this._stopRecording === null;
|
|
7324
|
+
};
|
|
7325
|
+
|
|
7306
7326
|
/**
|
|
7307
7327
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
7308
7328
|
* we stop recording and dump any queued events if the user has opted out.
|
|
7309
7329
|
*/
|
|
7310
|
-
|
|
7330
|
+
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
7311
7331
|
this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
|
|
7312
7332
|
};
|
|
7313
7333
|
|
|
7314
|
-
|
|
7334
|
+
SessionRecording.prototype._onOptOut = function (code) {
|
|
7315
7335
|
// addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
|
|
7316
7336
|
if (code === 0) {
|
|
7317
|
-
this.recEvents = [];
|
|
7318
7337
|
this.stopRecording();
|
|
7319
7338
|
}
|
|
7320
7339
|
};
|
|
7321
7340
|
|
|
7322
|
-
|
|
7341
|
+
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
7323
7342
|
var onSuccess = _.bind(function (response, responseBody) {
|
|
7324
|
-
//
|
|
7343
|
+
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
7325
7344
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
7326
7345
|
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
7327
7346
|
if (response.status === 200 && this.replayId === currentReplayId) {
|
|
7328
7347
|
this.seqNo++;
|
|
7348
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7329
7349
|
}
|
|
7330
7350
|
callback({
|
|
7331
7351
|
status: 0,
|
|
@@ -7335,10 +7355,10 @@
|
|
|
7335
7355
|
});
|
|
7336
7356
|
}, this);
|
|
7337
7357
|
|
|
7338
|
-
win['fetch'](this.
|
|
7358
|
+
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
7339
7359
|
'method': 'POST',
|
|
7340
7360
|
'headers': {
|
|
7341
|
-
'Authorization': 'Basic ' + btoa(this.
|
|
7361
|
+
'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
|
|
7342
7362
|
'Content-Type': 'application/octet-stream'
|
|
7343
7363
|
},
|
|
7344
7364
|
'body': reqBody,
|
|
@@ -7353,7 +7373,7 @@
|
|
|
7353
7373
|
});
|
|
7354
7374
|
};
|
|
7355
7375
|
|
|
7356
|
-
|
|
7376
|
+
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
7357
7377
|
const numEvents = data.length;
|
|
7358
7378
|
|
|
7359
7379
|
if (numEvents > 0) {
|
|
@@ -7371,12 +7391,15 @@
|
|
|
7371
7391
|
var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
|
|
7372
7392
|
|
|
7373
7393
|
var reqParams = {
|
|
7374
|
-
'
|
|
7375
|
-
'
|
|
7394
|
+
'$current_url': this.batchStartUrl,
|
|
7395
|
+
'$lib_version': Config.LIB_VERSION,
|
|
7376
7396
|
'batch_start_time': batchStartTime / 1000,
|
|
7397
|
+
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
7398
|
+
'mp_lib': 'web',
|
|
7377
7399
|
'replay_id': replayId,
|
|
7378
7400
|
'replay_length_ms': replayLengthMs,
|
|
7379
|
-
'replay_start_time': this.replayStartTime / 1000
|
|
7401
|
+
'replay_start_time': this.replayStartTime / 1000,
|
|
7402
|
+
'seq': this.seqNo
|
|
7380
7403
|
};
|
|
7381
7404
|
var eventsJson = _.JSONEncode(data);
|
|
7382
7405
|
|
|
@@ -7407,18 +7430,83 @@
|
|
|
7407
7430
|
});
|
|
7408
7431
|
|
|
7409
7432
|
|
|
7410
|
-
|
|
7411
|
-
logger.error.apply(logger.error, arguments);
|
|
7433
|
+
SessionRecording.prototype.reportError = function(msg, err) {
|
|
7434
|
+
logger$1.error.apply(logger$1.error, arguments);
|
|
7412
7435
|
try {
|
|
7413
7436
|
if (!err && !(msg instanceof Error)) {
|
|
7414
7437
|
msg = new Error(msg);
|
|
7415
7438
|
}
|
|
7416
|
-
this.
|
|
7439
|
+
this.getConfig('error_reporter')(msg, err);
|
|
7417
7440
|
} catch(err) {
|
|
7418
|
-
logger.error(err);
|
|
7441
|
+
logger$1.error(err);
|
|
7419
7442
|
}
|
|
7420
7443
|
};
|
|
7421
7444
|
|
|
7445
|
+
var logger = console_with_prefix('recorder');
|
|
7446
|
+
|
|
7447
|
+
/**
|
|
7448
|
+
* Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
|
|
7449
|
+
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7450
|
+
*/
|
|
7451
|
+
var MixpanelRecorder = function(mixpanelInstance) {
|
|
7452
|
+
this._mixpanel = mixpanelInstance;
|
|
7453
|
+
this.activeRecording = null;
|
|
7454
|
+
};
|
|
7455
|
+
|
|
7456
|
+
MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
|
|
7457
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7458
|
+
logger.log('Recording already in progress, skipping startRecording.');
|
|
7459
|
+
return;
|
|
7460
|
+
}
|
|
7461
|
+
|
|
7462
|
+
var onIdleTimeout = _.bind(function () {
|
|
7463
|
+
logger.log('Idle timeout reached, restarting recording.');
|
|
7464
|
+
this.resetRecording();
|
|
7465
|
+
}, this);
|
|
7466
|
+
|
|
7467
|
+
var onMaxLengthReached = _.bind(function () {
|
|
7468
|
+
logger.log('Max recording length reached, stopping recording.');
|
|
7469
|
+
this.resetRecording();
|
|
7470
|
+
}, this);
|
|
7471
|
+
|
|
7472
|
+
this.activeRecording = new SessionRecording({
|
|
7473
|
+
mixpanelInstance: this._mixpanel,
|
|
7474
|
+
onIdleTimeout: onIdleTimeout,
|
|
7475
|
+
onMaxLengthReached: onMaxLengthReached,
|
|
7476
|
+
replayId: _.UUID(),
|
|
7477
|
+
rrwebRecord: record
|
|
7478
|
+
});
|
|
7479
|
+
|
|
7480
|
+
this.activeRecording.startRecording(shouldStopBatcher);
|
|
7481
|
+
};
|
|
7482
|
+
|
|
7483
|
+
MixpanelRecorder.prototype.stopRecording = function() {
|
|
7484
|
+
if (this.activeRecording) {
|
|
7485
|
+
this.activeRecording.stopRecording();
|
|
7486
|
+
this.activeRecording = null;
|
|
7487
|
+
}
|
|
7488
|
+
};
|
|
7489
|
+
|
|
7490
|
+
MixpanelRecorder.prototype.resetRecording = function () {
|
|
7491
|
+
this.stopRecording();
|
|
7492
|
+
this.startRecording(true);
|
|
7493
|
+
};
|
|
7494
|
+
|
|
7495
|
+
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
7496
|
+
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7497
|
+
return this.activeRecording.replayId;
|
|
7498
|
+
} else {
|
|
7499
|
+
return null;
|
|
7500
|
+
}
|
|
7501
|
+
};
|
|
7502
|
+
|
|
7503
|
+
// getter so that older mixpanel-core versions can still retrieve the replay ID
|
|
7504
|
+
// when pulling the latest recorder bundle from the CDN
|
|
7505
|
+
Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
|
|
7506
|
+
get: function () {
|
|
7507
|
+
return this.getActiveReplayId();
|
|
7508
|
+
}
|
|
7509
|
+
});
|
|
7422
7510
|
|
|
7423
7511
|
win['__mp_recorder'] = MixpanelRecorder;
|
|
7424
7512
|
|