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 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.55.1'
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.storage = options.storage || window.localStorage;
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
- if (this._recorder) {
4514
- var replay_id = this._recorder['replayId'];
4515
- if (replay_id) {
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.55.1'
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$3 = console_with_prefix('lock');
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$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
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$2 = console_with_prefix('batch');
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$1 = console_with_prefix('batch');
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$1.log('Flush: Request already in progress');
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$1.log('MIXPANEL REQUEST:', dataForRequest);
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$1.error.apply(logger$1.error, arguments);
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$1.error(err);
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
- var MixpanelRecorder = function(mixpanelInstance) {
7173
- this._mixpanel = mixpanelInstance;
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.sendBatchId = null;
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
- MixpanelRecorder.prototype._initBatcher = function () {
7194
- this.batcher = new RequestBatcher('__mprec', {
7195
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
7196
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
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
- // eslint-disable-next-line camelcase
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
- MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
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.get_config('record_max_ms');
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.get_config('record_min_ms');
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(_.bind(function () {
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.get_config('record_block_selector');
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 = record({
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.get_config('record_block_class'),
7279
+ 'blockClass': this.getConfig('record_block_class'),
7268
7280
  'blockSelector': blockSelector,
7269
- 'collectFonts': this.get_config('record_collect_fonts'),
7270
- 'inlineImages': this.get_config('record_inline_images'),
7281
+ 'collectFonts': this.getConfig('record_collect_fonts'),
7271
7282
  'maskAllInputs': true,
7272
- 'maskTextClass': this.get_config('record_mask_text_class'),
7273
- 'maskTextSelector': this.get_config('record_mask_text_selector')
7283
+ 'maskTextClass': this.getConfig('record_mask_text_class'),
7284
+ 'maskTextSelector': this.getConfig('record_mask_text_selector')
7274
7285
  });
7275
7286
 
7276
- resetIdleTimeout();
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
- this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
7279
- };
7294
+ resetIdleTimeout();
7280
7295
 
7281
- MixpanelRecorder.prototype.resetRecording = function () {
7282
- this.stopRecording();
7283
- this.startRecording(true);
7296
+ this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
7284
7297
  };
7285
7298
 
7286
- MixpanelRecorder.prototype.stopRecording = function () {
7287
- if (this._stopRecording !== null) {
7288
- this._stopRecording();
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
- MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
7330
+ SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
7311
7331
  this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
7312
7332
  };
7313
7333
 
7314
- MixpanelRecorder.prototype._onOptOut = function (code) {
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
- MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7341
+ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7323
7342
  var onSuccess = _.bind(function (response, responseBody) {
7324
- // Increment sequence counter only if the request was successful to guarantee ordering.
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.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
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.get_config('token') + ':'),
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
- MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
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
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
7375
- 'seq': this.seqNo,
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
- MixpanelRecorder.prototype.reportError = function(msg, err) {
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.get_config('error_reporter')(msg, err);
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