mixpanel-browser 2.59.0 → 2.61.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/CHANGELOG.md +5 -1
- package/README.md +3 -3
- package/dist/mixpanel-core.cjs.js +612 -176
- package/dist/mixpanel-recorder.js +670 -224
- package/dist/mixpanel-recorder.min.js +11 -11
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +612 -176
- package/dist/mixpanel.amd.js +1000 -290
- package/dist/mixpanel.cjs.js +1000 -290
- package/dist/mixpanel.globals.js +612 -176
- package/dist/mixpanel.min.js +143 -134
- package/dist/mixpanel.module.js +1000 -290
- package/dist/mixpanel.umd.js +1000 -290
- package/package.json +2 -1
- package/src/autocapture/index.js +80 -9
- package/src/autocapture/utils.js +129 -38
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +119 -19
- package/src/mixpanel-persistence.js +6 -2
- package/src/recorder/index.js +1 -70
- package/src/recorder/recorder.js +137 -0
- package/src/recorder/recording-registry.js +98 -0
- package/src/recorder/session-recording.js +162 -43
- package/src/recorder/utils.js +12 -0
- package/src/request-batcher.js +6 -2
- package/src/request-queue.js +45 -39
- package/src/shared-lock.js +1 -1
- package/src/storage/indexed-db.js +127 -0
- package/src/storage/local-storage.js +4 -8
- package/src/storage/wrapper.js +3 -3
- package/src/utils.js +99 -61
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
5
|
+
var win;
|
|
6
|
+
if (typeof(window) === 'undefined') {
|
|
7
|
+
var loc = {
|
|
8
|
+
hostname: ''
|
|
9
|
+
};
|
|
10
|
+
win = {
|
|
11
|
+
navigator: { userAgent: '', onLine: true },
|
|
12
|
+
document: {
|
|
13
|
+
createElement: function() { return {}; },
|
|
14
|
+
location: loc,
|
|
15
|
+
referrer: ''
|
|
16
|
+
},
|
|
17
|
+
screen: { width: 0, height: 0 },
|
|
18
|
+
location: loc,
|
|
19
|
+
addEventListener: function() {},
|
|
20
|
+
removeEventListener: function() {}
|
|
21
|
+
};
|
|
22
|
+
} else {
|
|
23
|
+
win = window;
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
var NodeType;
|
|
5
27
|
(function (NodeType) {
|
|
6
28
|
NodeType[NodeType["Document"] = 0] = "Document";
|
|
@@ -4477,64 +4499,6 @@
|
|
|
4477
4499
|
};
|
|
4478
4500
|
record.mirror = mirror;
|
|
4479
4501
|
|
|
4480
|
-
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
4481
|
-
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
|
|
4482
|
-
EventType2[EventType2["Load"] = 1] = "Load";
|
|
4483
|
-
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
|
|
4484
|
-
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
|
|
4485
|
-
EventType2[EventType2["Meta"] = 4] = "Meta";
|
|
4486
|
-
EventType2[EventType2["Custom"] = 5] = "Custom";
|
|
4487
|
-
EventType2[EventType2["Plugin"] = 6] = "Plugin";
|
|
4488
|
-
return EventType2;
|
|
4489
|
-
})(EventType || {});
|
|
4490
|
-
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
4491
|
-
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
|
|
4492
|
-
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
|
|
4493
|
-
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
|
|
4494
|
-
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
|
|
4495
|
-
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
|
|
4496
|
-
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
|
|
4497
|
-
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
|
|
4498
|
-
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
|
|
4499
|
-
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
|
|
4500
|
-
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
|
|
4501
|
-
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
|
|
4502
|
-
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
|
|
4503
|
-
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
|
|
4504
|
-
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
|
|
4505
|
-
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
|
|
4506
|
-
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
|
|
4507
|
-
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
|
|
4508
|
-
return IncrementalSource2;
|
|
4509
|
-
})(IncrementalSource || {});
|
|
4510
|
-
|
|
4511
|
-
var Config = {
|
|
4512
|
-
DEBUG: false,
|
|
4513
|
-
LIB_VERSION: '2.59.0'
|
|
4514
|
-
};
|
|
4515
|
-
|
|
4516
|
-
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
4517
|
-
var win;
|
|
4518
|
-
if (typeof(window) === 'undefined') {
|
|
4519
|
-
var loc = {
|
|
4520
|
-
hostname: ''
|
|
4521
|
-
};
|
|
4522
|
-
win = {
|
|
4523
|
-
navigator: { userAgent: '', onLine: true },
|
|
4524
|
-
document: {
|
|
4525
|
-
createElement: function() { return {}; },
|
|
4526
|
-
location: loc,
|
|
4527
|
-
referrer: ''
|
|
4528
|
-
},
|
|
4529
|
-
screen: { width: 0, height: 0 },
|
|
4530
|
-
location: loc,
|
|
4531
|
-
addEventListener: function() {},
|
|
4532
|
-
removeEventListener: function() {}
|
|
4533
|
-
};
|
|
4534
|
-
} else {
|
|
4535
|
-
win = window;
|
|
4536
|
-
}
|
|
4537
|
-
|
|
4538
4502
|
var setImmediate = win['setImmediate'];
|
|
4539
4503
|
var builtInProp, cycle, schedulingQueue,
|
|
4540
4504
|
ToString = Object.prototype.toString,
|
|
@@ -4897,6 +4861,42 @@
|
|
|
4897
4861
|
PromisePolyfill = NpoPromise;
|
|
4898
4862
|
}
|
|
4899
4863
|
|
|
4864
|
+
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
4865
|
+
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
|
|
4866
|
+
EventType2[EventType2["Load"] = 1] = "Load";
|
|
4867
|
+
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
|
|
4868
|
+
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
|
|
4869
|
+
EventType2[EventType2["Meta"] = 4] = "Meta";
|
|
4870
|
+
EventType2[EventType2["Custom"] = 5] = "Custom";
|
|
4871
|
+
EventType2[EventType2["Plugin"] = 6] = "Plugin";
|
|
4872
|
+
return EventType2;
|
|
4873
|
+
})(EventType || {});
|
|
4874
|
+
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
4875
|
+
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
|
|
4876
|
+
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
|
|
4877
|
+
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
|
|
4878
|
+
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
|
|
4879
|
+
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
|
|
4880
|
+
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
|
|
4881
|
+
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
|
|
4882
|
+
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
|
|
4883
|
+
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
|
|
4884
|
+
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
|
|
4885
|
+
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
|
|
4886
|
+
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
|
|
4887
|
+
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
|
|
4888
|
+
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
|
|
4889
|
+
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
|
|
4890
|
+
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
|
|
4891
|
+
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
|
|
4892
|
+
return IncrementalSource2;
|
|
4893
|
+
})(IncrementalSource || {});
|
|
4894
|
+
|
|
4895
|
+
var Config = {
|
|
4896
|
+
DEBUG: false,
|
|
4897
|
+
LIB_VERSION: '2.61.0'
|
|
4898
|
+
};
|
|
4899
|
+
|
|
4900
4900
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
4901
4901
|
|
|
4902
4902
|
// Maximum allowed session recording length
|
|
@@ -5924,15 +5924,9 @@
|
|
|
5924
5924
|
}
|
|
5925
5925
|
};
|
|
5926
5926
|
|
|
5927
|
-
var
|
|
5928
|
-
var localStorageSupported = function(storage, forceCheck) {
|
|
5929
|
-
if (_localStorageSupported !== null && !forceCheck) {
|
|
5930
|
-
return _localStorageSupported;
|
|
5931
|
-
}
|
|
5932
|
-
|
|
5927
|
+
var _testStorageSupported = function (storage) {
|
|
5933
5928
|
var supported = true;
|
|
5934
5929
|
try {
|
|
5935
|
-
storage = storage || win.localStorage;
|
|
5936
5930
|
var key = '__mplss_' + cheap_guid(8),
|
|
5937
5931
|
val = 'xyz';
|
|
5938
5932
|
storage.setItem(key, val);
|
|
@@ -5943,59 +5937,74 @@
|
|
|
5943
5937
|
} catch (err) {
|
|
5944
5938
|
supported = false;
|
|
5945
5939
|
}
|
|
5946
|
-
|
|
5947
|
-
_localStorageSupported = supported;
|
|
5948
5940
|
return supported;
|
|
5949
5941
|
};
|
|
5950
5942
|
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
return supported;
|
|
5959
|
-
},
|
|
5960
|
-
|
|
5961
|
-
error: function(msg) {
|
|
5962
|
-
console$1.error('localStorage error: ' + msg);
|
|
5963
|
-
},
|
|
5943
|
+
var _localStorageSupported = null;
|
|
5944
|
+
var localStorageSupported = function(storage, forceCheck) {
|
|
5945
|
+
if (_localStorageSupported !== null && !forceCheck) {
|
|
5946
|
+
return _localStorageSupported;
|
|
5947
|
+
}
|
|
5948
|
+
return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
|
|
5949
|
+
};
|
|
5964
5950
|
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
},
|
|
5951
|
+
var _sessionStorageSupported = null;
|
|
5952
|
+
var sessionStorageSupported = function(storage, forceCheck) {
|
|
5953
|
+
if (_sessionStorageSupported !== null && !forceCheck) {
|
|
5954
|
+
return _sessionStorageSupported;
|
|
5955
|
+
}
|
|
5956
|
+
return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
|
|
5957
|
+
};
|
|
5973
5958
|
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
// noop
|
|
5979
|
-
}
|
|
5980
|
-
return null;
|
|
5981
|
-
},
|
|
5959
|
+
function _storageWrapper(storage, name, is_supported_fn) {
|
|
5960
|
+
var log_error = function(msg) {
|
|
5961
|
+
console$1.error(name + ' error: ' + msg);
|
|
5962
|
+
};
|
|
5982
5963
|
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5964
|
+
return {
|
|
5965
|
+
is_supported: function(forceCheck) {
|
|
5966
|
+
var supported = is_supported_fn(storage, forceCheck);
|
|
5967
|
+
if (!supported) {
|
|
5968
|
+
console$1.error(name + ' unsupported');
|
|
5969
|
+
}
|
|
5970
|
+
return supported;
|
|
5971
|
+
},
|
|
5972
|
+
error: log_error,
|
|
5973
|
+
get: function(key) {
|
|
5974
|
+
try {
|
|
5975
|
+
return storage.getItem(key);
|
|
5976
|
+
} catch (err) {
|
|
5977
|
+
log_error(err);
|
|
5978
|
+
}
|
|
5979
|
+
return null;
|
|
5980
|
+
},
|
|
5981
|
+
parse: function(key) {
|
|
5982
|
+
try {
|
|
5983
|
+
return _.JSONDecode(storage.getItem(key)) || {};
|
|
5984
|
+
} catch (err) {
|
|
5985
|
+
// noop
|
|
5986
|
+
}
|
|
5987
|
+
return null;
|
|
5988
|
+
},
|
|
5989
|
+
set: function(key, value) {
|
|
5990
|
+
try {
|
|
5991
|
+
storage.setItem(key, value);
|
|
5992
|
+
} catch (err) {
|
|
5993
|
+
log_error(err);
|
|
5994
|
+
}
|
|
5995
|
+
},
|
|
5996
|
+
remove: function(key) {
|
|
5997
|
+
try {
|
|
5998
|
+
storage.removeItem(key);
|
|
5999
|
+
} catch (err) {
|
|
6000
|
+
log_error(err);
|
|
6001
|
+
}
|
|
5988
6002
|
}
|
|
5989
|
-
}
|
|
6003
|
+
};
|
|
6004
|
+
}
|
|
5990
6005
|
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
win.localStorage.removeItem(name);
|
|
5994
|
-
} catch (err) {
|
|
5995
|
-
_.localStorage.error(err);
|
|
5996
|
-
}
|
|
5997
|
-
}
|
|
5998
|
-
};
|
|
6006
|
+
_.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
|
|
6007
|
+
_.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
|
|
5999
6008
|
|
|
6000
6009
|
_.register_event = (function() {
|
|
6001
6010
|
// written by Dean Edwards, 2005
|
|
@@ -6522,6 +6531,31 @@
|
|
|
6522
6531
|
}
|
|
6523
6532
|
};
|
|
6524
6533
|
|
|
6534
|
+
/**
|
|
6535
|
+
* Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
|
|
6536
|
+
* Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
|
|
6537
|
+
*/
|
|
6538
|
+
var batchedThrottle = function (fn, waitMs) {
|
|
6539
|
+
var timeoutPromise = null;
|
|
6540
|
+
var throttledItems = [];
|
|
6541
|
+
return function (item) {
|
|
6542
|
+
var self = this;
|
|
6543
|
+
throttledItems.push(item);
|
|
6544
|
+
|
|
6545
|
+
if (!timeoutPromise) {
|
|
6546
|
+
timeoutPromise = new PromisePolyfill(function (resolve) {
|
|
6547
|
+
setTimeout(function () {
|
|
6548
|
+
var returnValue = fn.apply(self, [throttledItems]);
|
|
6549
|
+
timeoutPromise = null;
|
|
6550
|
+
throttledItems = [];
|
|
6551
|
+
resolve(returnValue);
|
|
6552
|
+
}, waitMs);
|
|
6553
|
+
});
|
|
6554
|
+
}
|
|
6555
|
+
return timeoutPromise;
|
|
6556
|
+
};
|
|
6557
|
+
};
|
|
6558
|
+
|
|
6525
6559
|
var cheap_guid = function(maxlen) {
|
|
6526
6560
|
var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
|
|
6527
6561
|
return maxlen ? guid.substring(0, maxlen) : guid;
|
|
@@ -6564,6 +6598,8 @@
|
|
|
6564
6598
|
return _.isUndefined(onLine) || onLine;
|
|
6565
6599
|
};
|
|
6566
6600
|
|
|
6601
|
+
var NOOP_FUNC = function () {};
|
|
6602
|
+
|
|
6567
6603
|
var JSONStringify = null, JSONParse = null;
|
|
6568
6604
|
if (typeof JSON !== 'undefined') {
|
|
6569
6605
|
JSONStringify = JSON.stringify;
|
|
@@ -6572,20 +6608,143 @@
|
|
|
6572
6608
|
JSONStringify = JSONStringify || _.JSONEncode;
|
|
6573
6609
|
JSONParse = JSONParse || _.JSONDecode;
|
|
6574
6610
|
|
|
6575
|
-
// EXPORTS (for closure compiler)
|
|
6576
|
-
_['toArray'] = _.toArray;
|
|
6577
|
-
_['isObject'] = _.isObject;
|
|
6578
|
-
_['JSONEncode'] = _.JSONEncode;
|
|
6579
|
-
_['JSONDecode'] = _.JSONDecode;
|
|
6580
|
-
_['isBlockedUA'] = _.isBlockedUA;
|
|
6581
|
-
_['isEmptyObject'] = _.isEmptyObject;
|
|
6611
|
+
// UNMINIFIED EXPORTS (for closure compiler)
|
|
6582
6612
|
_['info'] = _.info;
|
|
6583
|
-
_['info']['device'] = _.info.device;
|
|
6584
6613
|
_['info']['browser'] = _.info.browser;
|
|
6585
6614
|
_['info']['browserVersion'] = _.info.browserVersion;
|
|
6615
|
+
_['info']['device'] = _.info.device;
|
|
6586
6616
|
_['info']['properties'] = _.info.properties;
|
|
6617
|
+
_['isBlockedUA'] = _.isBlockedUA;
|
|
6618
|
+
_['isEmptyObject'] = _.isEmptyObject;
|
|
6619
|
+
_['isObject'] = _.isObject;
|
|
6620
|
+
_['JSONDecode'] = _.JSONDecode;
|
|
6621
|
+
_['JSONEncode'] = _.JSONEncode;
|
|
6622
|
+
_['toArray'] = _.toArray;
|
|
6587
6623
|
_['NPO'] = NpoPromise;
|
|
6588
6624
|
|
|
6625
|
+
var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
|
|
6626
|
+
|
|
6627
|
+
var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
|
|
6628
|
+
var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
|
|
6629
|
+
|
|
6630
|
+
// note: increment the version number when adding new object stores
|
|
6631
|
+
var DB_VERSION = 1;
|
|
6632
|
+
var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
|
|
6633
|
+
|
|
6634
|
+
/**
|
|
6635
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
6636
|
+
*/
|
|
6637
|
+
var IDBStorageWrapper = function (storeName) {
|
|
6638
|
+
/**
|
|
6639
|
+
* @type {Promise<IDBDatabase>|null}
|
|
6640
|
+
*/
|
|
6641
|
+
this.dbPromise = null;
|
|
6642
|
+
this.storeName = storeName;
|
|
6643
|
+
};
|
|
6644
|
+
|
|
6645
|
+
IDBStorageWrapper.prototype._openDb = function () {
|
|
6646
|
+
return new PromisePolyfill(function (resolve, reject) {
|
|
6647
|
+
var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
|
|
6648
|
+
openRequest['onerror'] = function () {
|
|
6649
|
+
reject(openRequest.error);
|
|
6650
|
+
};
|
|
6651
|
+
|
|
6652
|
+
openRequest['onsuccess'] = function () {
|
|
6653
|
+
resolve(openRequest.result);
|
|
6654
|
+
};
|
|
6655
|
+
|
|
6656
|
+
openRequest['onupgradeneeded'] = function (ev) {
|
|
6657
|
+
var db = ev.target.result;
|
|
6658
|
+
|
|
6659
|
+
OBJECT_STORES.forEach(function (storeName) {
|
|
6660
|
+
db.createObjectStore(storeName);
|
|
6661
|
+
});
|
|
6662
|
+
};
|
|
6663
|
+
});
|
|
6664
|
+
};
|
|
6665
|
+
|
|
6666
|
+
IDBStorageWrapper.prototype.init = function () {
|
|
6667
|
+
if (!win.indexedDB) {
|
|
6668
|
+
return PromisePolyfill.reject('indexedDB is not supported in this browser');
|
|
6669
|
+
}
|
|
6670
|
+
|
|
6671
|
+
if (!this.dbPromise) {
|
|
6672
|
+
this.dbPromise = this._openDb();
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
return this.dbPromise
|
|
6676
|
+
.then(function (dbOrError) {
|
|
6677
|
+
if (dbOrError instanceof win['IDBDatabase']) {
|
|
6678
|
+
return PromisePolyfill.resolve();
|
|
6679
|
+
} else {
|
|
6680
|
+
return PromisePolyfill.reject(dbOrError);
|
|
6681
|
+
}
|
|
6682
|
+
});
|
|
6683
|
+
};
|
|
6684
|
+
|
|
6685
|
+
/**
|
|
6686
|
+
* @param {IDBTransactionMode} mode
|
|
6687
|
+
* @param {function(IDBObjectStore): void} storeCb
|
|
6688
|
+
*/
|
|
6689
|
+
IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
|
|
6690
|
+
var storeName = this.storeName;
|
|
6691
|
+
var doTransaction = function (db) {
|
|
6692
|
+
return new PromisePolyfill(function (resolve, reject) {
|
|
6693
|
+
var transaction = db.transaction(storeName, mode);
|
|
6694
|
+
transaction.oncomplete = function () {
|
|
6695
|
+
resolve(transaction);
|
|
6696
|
+
};
|
|
6697
|
+
transaction.onabort = transaction.onerror = function () {
|
|
6698
|
+
reject(transaction.error);
|
|
6699
|
+
};
|
|
6700
|
+
|
|
6701
|
+
storeCb(transaction.objectStore(storeName));
|
|
6702
|
+
});
|
|
6703
|
+
};
|
|
6704
|
+
|
|
6705
|
+
return this.dbPromise
|
|
6706
|
+
.then(doTransaction)
|
|
6707
|
+
.catch(function (err) {
|
|
6708
|
+
if (err['name'] === 'InvalidStateError') {
|
|
6709
|
+
// try reopening the DB if the connection is closed
|
|
6710
|
+
this.dbPromise = this._openDb();
|
|
6711
|
+
return this.dbPromise.then(doTransaction);
|
|
6712
|
+
} else {
|
|
6713
|
+
return PromisePolyfill.reject(err);
|
|
6714
|
+
}
|
|
6715
|
+
}.bind(this));
|
|
6716
|
+
};
|
|
6717
|
+
|
|
6718
|
+
IDBStorageWrapper.prototype.setItem = function (key, value) {
|
|
6719
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
6720
|
+
objectStore.put(value, key);
|
|
6721
|
+
});
|
|
6722
|
+
};
|
|
6723
|
+
|
|
6724
|
+
IDBStorageWrapper.prototype.getItem = function (key) {
|
|
6725
|
+
var req;
|
|
6726
|
+
return this.makeTransaction('readonly', function (objectStore) {
|
|
6727
|
+
req = objectStore.get(key);
|
|
6728
|
+
}).then(function () {
|
|
6729
|
+
return req.result;
|
|
6730
|
+
});
|
|
6731
|
+
};
|
|
6732
|
+
|
|
6733
|
+
IDBStorageWrapper.prototype.removeItem = function (key) {
|
|
6734
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
6735
|
+
objectStore.delete(key);
|
|
6736
|
+
});
|
|
6737
|
+
};
|
|
6738
|
+
|
|
6739
|
+
IDBStorageWrapper.prototype.getAll = function () {
|
|
6740
|
+
var req;
|
|
6741
|
+
return this.makeTransaction('readonly', function (objectStore) {
|
|
6742
|
+
req = objectStore.getAll();
|
|
6743
|
+
}).then(function () {
|
|
6744
|
+
return req.result;
|
|
6745
|
+
});
|
|
6746
|
+
};
|
|
6747
|
+
|
|
6589
6748
|
/**
|
|
6590
6749
|
* GDPR utils
|
|
6591
6750
|
*
|
|
@@ -6779,7 +6938,7 @@
|
|
|
6779
6938
|
options = options || {};
|
|
6780
6939
|
|
|
6781
6940
|
this.storageKey = key;
|
|
6782
|
-
this.storage = options.storage ||
|
|
6941
|
+
this.storage = options.storage || win.localStorage;
|
|
6783
6942
|
this.pollIntervalMS = options.pollIntervalMS || 100;
|
|
6784
6943
|
this.timeoutMS = options.timeoutMS || 2000;
|
|
6785
6944
|
|
|
@@ -6794,7 +6953,6 @@
|
|
|
6794
6953
|
return new Promise(_.bind(function (resolve, reject) {
|
|
6795
6954
|
var i = pid || (new Date().getTime() + '|' + Math.random());
|
|
6796
6955
|
var startTime = new Date().getTime();
|
|
6797
|
-
|
|
6798
6956
|
var key = this.storageKey;
|
|
6799
6957
|
var pollIntervalMS = this.pollIntervalMS;
|
|
6800
6958
|
var timeoutMS = this.timeoutMS;
|
|
@@ -6905,11 +7063,7 @@
|
|
|
6905
7063
|
};
|
|
6906
7064
|
|
|
6907
7065
|
/**
|
|
6908
|
-
* @
|
|
6909
|
-
*/
|
|
6910
|
-
|
|
6911
|
-
/**
|
|
6912
|
-
* @type {StorageWrapper}
|
|
7066
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
6913
7067
|
*/
|
|
6914
7068
|
var LocalStorageWrapper = function (storageOverride) {
|
|
6915
7069
|
this.storage = storageOverride || localStorage;
|
|
@@ -6922,7 +7076,7 @@
|
|
|
6922
7076
|
LocalStorageWrapper.prototype.setItem = function (key, value) {
|
|
6923
7077
|
return new PromisePolyfill(_.bind(function (resolve, reject) {
|
|
6924
7078
|
try {
|
|
6925
|
-
this.storage.setItem(key, value);
|
|
7079
|
+
this.storage.setItem(key, JSONStringify(value));
|
|
6926
7080
|
} catch (e) {
|
|
6927
7081
|
reject(e);
|
|
6928
7082
|
}
|
|
@@ -6934,7 +7088,7 @@
|
|
|
6934
7088
|
return new PromisePolyfill(_.bind(function (resolve, reject) {
|
|
6935
7089
|
var item;
|
|
6936
7090
|
try {
|
|
6937
|
-
item = this.storage.getItem(key);
|
|
7091
|
+
item = JSONParse(this.storage.getItem(key));
|
|
6938
7092
|
} catch (e) {
|
|
6939
7093
|
reject(e);
|
|
6940
7094
|
}
|
|
@@ -6977,8 +7131,10 @@
|
|
|
6977
7131
|
this.usePersistence = options.usePersistence;
|
|
6978
7132
|
if (this.usePersistence) {
|
|
6979
7133
|
this.queueStorage = options.queueStorage || new LocalStorageWrapper();
|
|
6980
|
-
this.lock = new SharedLock(storageKey, {
|
|
6981
|
-
|
|
7134
|
+
this.lock = new SharedLock(storageKey, {
|
|
7135
|
+
storage: options.sharedLockStorage || win.localStorage,
|
|
7136
|
+
timeoutMS: options.sharedLockTimeoutMS,
|
|
7137
|
+
});
|
|
6982
7138
|
}
|
|
6983
7139
|
this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
|
|
6984
7140
|
|
|
@@ -6986,6 +7142,14 @@
|
|
|
6986
7142
|
|
|
6987
7143
|
this.memQueue = [];
|
|
6988
7144
|
this.initialized = false;
|
|
7145
|
+
|
|
7146
|
+
if (options.enqueueThrottleMs) {
|
|
7147
|
+
this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
|
|
7148
|
+
} else {
|
|
7149
|
+
this.enqueuePersisted = _.bind(function (queueEntry) {
|
|
7150
|
+
return this._enqueuePersisted([queueEntry]);
|
|
7151
|
+
}, this);
|
|
7152
|
+
}
|
|
6989
7153
|
};
|
|
6990
7154
|
|
|
6991
7155
|
RequestQueue.prototype.ensureInit = function () {
|
|
@@ -7028,36 +7192,39 @@
|
|
|
7028
7192
|
this.memQueue.push(queueEntry);
|
|
7029
7193
|
return PromisePolyfill.resolve(true);
|
|
7030
7194
|
} else {
|
|
7195
|
+
return this.enqueuePersisted(queueEntry);
|
|
7196
|
+
}
|
|
7197
|
+
};
|
|
7031
7198
|
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
return succeeded;
|
|
7047
|
-
}, this))
|
|
7048
|
-
.catch(_.bind(function (err) {
|
|
7049
|
-
this.reportError('Error enqueueing item', err, item);
|
|
7050
|
-
return false;
|
|
7051
|
-
}, this));
|
|
7052
|
-
}, this);
|
|
7199
|
+
RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
|
|
7200
|
+
var enqueueItem = _.bind(function () {
|
|
7201
|
+
return this.ensureInit()
|
|
7202
|
+
.then(_.bind(function () {
|
|
7203
|
+
return this.readFromStorage();
|
|
7204
|
+
}, this))
|
|
7205
|
+
.then(_.bind(function (storedQueue) {
|
|
7206
|
+
return this.saveToStorage(storedQueue.concat(queueEntries));
|
|
7207
|
+
}, this))
|
|
7208
|
+
.then(_.bind(function (succeeded) {
|
|
7209
|
+
// only add to in-memory queue when storage succeeds
|
|
7210
|
+
if (succeeded) {
|
|
7211
|
+
this.memQueue = this.memQueue.concat(queueEntries);
|
|
7212
|
+
}
|
|
7053
7213
|
|
|
7054
|
-
|
|
7055
|
-
|
|
7214
|
+
return succeeded;
|
|
7215
|
+
}, this))
|
|
7056
7216
|
.catch(_.bind(function (err) {
|
|
7057
|
-
this.reportError('Error
|
|
7217
|
+
this.reportError('Error enqueueing items', err, queueEntries);
|
|
7058
7218
|
return false;
|
|
7059
7219
|
}, this));
|
|
7060
|
-
}
|
|
7220
|
+
}, this);
|
|
7221
|
+
|
|
7222
|
+
return this.lock
|
|
7223
|
+
.withLock(enqueueItem, this.pid)
|
|
7224
|
+
.catch(_.bind(function (err) {
|
|
7225
|
+
this.reportError('Error acquiring storage lock', err);
|
|
7226
|
+
return false;
|
|
7227
|
+
}, this));
|
|
7061
7228
|
};
|
|
7062
7229
|
|
|
7063
7230
|
/**
|
|
@@ -7078,7 +7245,7 @@
|
|
|
7078
7245
|
}, this))
|
|
7079
7246
|
.then(_.bind(function (storedQueue) {
|
|
7080
7247
|
if (storedQueue.length) {
|
|
7081
|
-
|
|
7248
|
+
// item IDs already in batch; don't duplicate out of storage
|
|
7082
7249
|
var idsInBatch = {}; // poor man's Set
|
|
7083
7250
|
_.each(batch, function (item) {
|
|
7084
7251
|
idsInBatch[item['id']] = true;
|
|
@@ -7165,7 +7332,7 @@
|
|
|
7165
7332
|
.withLock(removeFromStorage, this.pid)
|
|
7166
7333
|
.catch(_.bind(function (err) {
|
|
7167
7334
|
this.reportError('Error acquiring storage lock', err);
|
|
7168
|
-
if (!localStorageSupported(this.
|
|
7335
|
+
if (!localStorageSupported(this.lock.storage, true)) {
|
|
7169
7336
|
// Looks like localStorage writes have stopped working sometime after
|
|
7170
7337
|
// initialization (probably full), and so nobody can acquire locks
|
|
7171
7338
|
// anymore. Consider it temporarily safe to remove items without the
|
|
@@ -7253,7 +7420,6 @@
|
|
|
7253
7420
|
}, this))
|
|
7254
7421
|
.then(_.bind(function (storageEntry) {
|
|
7255
7422
|
if (storageEntry) {
|
|
7256
|
-
storageEntry = JSONParse(storageEntry);
|
|
7257
7423
|
if (!_.isArray(storageEntry)) {
|
|
7258
7424
|
this.reportError('Invalid storage entry:', storageEntry);
|
|
7259
7425
|
storageEntry = null;
|
|
@@ -7271,16 +7437,9 @@
|
|
|
7271
7437
|
* Serialize the given items array to localStorage.
|
|
7272
7438
|
*/
|
|
7273
7439
|
RequestQueue.prototype.saveToStorage = function (queue) {
|
|
7274
|
-
try {
|
|
7275
|
-
var serialized = JSONStringify(queue);
|
|
7276
|
-
} catch (err) {
|
|
7277
|
-
this.reportError('Error serializing queue', err);
|
|
7278
|
-
return PromisePolyfill.resolve(false);
|
|
7279
|
-
}
|
|
7280
|
-
|
|
7281
7440
|
return this.ensureInit()
|
|
7282
7441
|
.then(_.bind(function () {
|
|
7283
|
-
return this.queueStorage.setItem(this.storageKey,
|
|
7442
|
+
return this.queueStorage.setItem(this.storageKey, queue);
|
|
7284
7443
|
}, this))
|
|
7285
7444
|
.then(function () {
|
|
7286
7445
|
return true;
|
|
@@ -7324,7 +7483,9 @@
|
|
|
7324
7483
|
errorReporter: _.bind(this.reportError, this),
|
|
7325
7484
|
queueStorage: options.queueStorage,
|
|
7326
7485
|
sharedLockStorage: options.sharedLockStorage,
|
|
7327
|
-
|
|
7486
|
+
sharedLockTimeoutMS: options.sharedLockTimeoutMS,
|
|
7487
|
+
usePersistence: options.usePersistence,
|
|
7488
|
+
enqueueThrottleMs: options.enqueueThrottleMs
|
|
7328
7489
|
});
|
|
7329
7490
|
|
|
7330
7491
|
this.libConfig = options.libConfig;
|
|
@@ -7346,6 +7507,8 @@
|
|
|
7346
7507
|
// as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
|
|
7347
7508
|
// in a request loop and get ratelimited by the server.
|
|
7348
7509
|
this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
|
|
7510
|
+
|
|
7511
|
+
this._flushPromise = null;
|
|
7349
7512
|
};
|
|
7350
7513
|
|
|
7351
7514
|
/**
|
|
@@ -7405,7 +7568,7 @@
|
|
|
7405
7568
|
if (!this.stopped) { // don't schedule anymore if batching has been stopped
|
|
7406
7569
|
this.timeoutID = setTimeout(_.bind(function() {
|
|
7407
7570
|
if (!this.stopped) {
|
|
7408
|
-
this.flush();
|
|
7571
|
+
this._flushPromise = this.flush();
|
|
7409
7572
|
}
|
|
7410
7573
|
}, this), this.flushInterval);
|
|
7411
7574
|
}
|
|
@@ -7637,6 +7800,17 @@
|
|
|
7637
7800
|
}
|
|
7638
7801
|
};
|
|
7639
7802
|
|
|
7803
|
+
/**
|
|
7804
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
7805
|
+
* @returns {boolean}
|
|
7806
|
+
*/
|
|
7807
|
+
var isRecordingExpired = function(serializedRecording) {
|
|
7808
|
+
var now = Date.now();
|
|
7809
|
+
return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
|
|
7810
|
+
};
|
|
7811
|
+
|
|
7812
|
+
var RECORD_ENQUEUE_THROTTLE_MS = 250;
|
|
7813
|
+
|
|
7640
7814
|
var logger$1 = console_with_prefix('recorder');
|
|
7641
7815
|
var CompressionStream = win['CompressionStream'];
|
|
7642
7816
|
|
|
@@ -7663,29 +7837,58 @@
|
|
|
7663
7837
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7664
7838
|
}
|
|
7665
7839
|
|
|
7840
|
+
/**
|
|
7841
|
+
* @typedef {Object} SerializedRecording
|
|
7842
|
+
* @property {number} idleExpires
|
|
7843
|
+
* @property {number} maxExpires
|
|
7844
|
+
* @property {number} replayStartTime
|
|
7845
|
+
* @property {number} seqNo
|
|
7846
|
+
* @property {string} batchStartUrl
|
|
7847
|
+
* @property {string} replayId
|
|
7848
|
+
* @property {string} tabId
|
|
7849
|
+
* @property {string} replayStartUrl
|
|
7850
|
+
*/
|
|
7851
|
+
|
|
7852
|
+
/**
|
|
7853
|
+
* @typedef {Object} SessionRecordingOptions
|
|
7854
|
+
* @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7855
|
+
* @property {String} [options.replayId] - unique uuid for a single replay
|
|
7856
|
+
* @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7857
|
+
* @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7858
|
+
* @property {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
7859
|
+
* @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
|
|
7860
|
+
* @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
|
|
7861
|
+
* optional properties for deserialization:
|
|
7862
|
+
* @property {number} idleExpires
|
|
7863
|
+
* @property {number} maxExpires
|
|
7864
|
+
* @property {number} replayStartTime
|
|
7865
|
+
* @property {number} seqNo
|
|
7866
|
+
* @property {string} batchStartUrl
|
|
7867
|
+
* @property {string} replayStartUrl
|
|
7868
|
+
*/
|
|
7869
|
+
|
|
7870
|
+
|
|
7666
7871
|
/**
|
|
7667
7872
|
* This class encapsulates a single session recording and its lifecycle.
|
|
7668
|
-
* @param {
|
|
7669
|
-
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7670
|
-
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7671
|
-
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7672
|
-
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
7873
|
+
* @param {SessionRecordingOptions} options
|
|
7673
7874
|
*/
|
|
7674
7875
|
var SessionRecording = function(options) {
|
|
7675
7876
|
this._mixpanel = options.mixpanelInstance;
|
|
7676
|
-
this._onIdleTimeout = options.onIdleTimeout;
|
|
7677
|
-
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7678
|
-
this.
|
|
7679
|
-
|
|
7680
|
-
this.replayId = options.replayId;
|
|
7877
|
+
this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
|
|
7878
|
+
this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
|
|
7879
|
+
this._onBatchSent = options.onBatchSent || NOOP_FUNC;
|
|
7880
|
+
this._rrwebRecord = options.rrwebRecord || null;
|
|
7681
7881
|
|
|
7682
7882
|
// internal rrweb stopRecording function
|
|
7683
7883
|
this._stopRecording = null;
|
|
7884
|
+
this.replayId = options.replayId;
|
|
7684
7885
|
|
|
7685
|
-
this.
|
|
7686
|
-
this.
|
|
7687
|
-
this.
|
|
7688
|
-
this.
|
|
7886
|
+
this.batchStartUrl = options.batchStartUrl || null;
|
|
7887
|
+
this.replayStartUrl = options.replayStartUrl || null;
|
|
7888
|
+
this.idleExpires = options.idleExpires || null;
|
|
7889
|
+
this.maxExpires = options.maxExpires || null;
|
|
7890
|
+
this.replayStartTime = options.replayStartTime || null;
|
|
7891
|
+
this.seqNo = options.seqNo || 0;
|
|
7689
7892
|
|
|
7690
7893
|
this.idleTimeoutId = null;
|
|
7691
7894
|
this.maxTimeoutId = null;
|
|
@@ -7693,18 +7896,40 @@
|
|
|
7693
7896
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7694
7897
|
this.recordMinMs = 0;
|
|
7695
7898
|
|
|
7899
|
+
// disable persistence if localStorage is not supported
|
|
7900
|
+
// request-queue will automatically disable persistence if indexedDB fails to initialize
|
|
7901
|
+
var usePersistence = localStorageSupported(options.sharedLockStorage, true);
|
|
7902
|
+
|
|
7696
7903
|
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7697
7904
|
// this will be important when persistence is introduced
|
|
7698
|
-
|
|
7699
|
-
this.
|
|
7700
|
-
|
|
7905
|
+
this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
|
|
7906
|
+
this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
|
|
7907
|
+
this.batcher = new RequestBatcher(this.batcherKey, {
|
|
7908
|
+
errorReporter: this.reportError.bind(this),
|
|
7701
7909
|
flushOnlyOnInterval: true,
|
|
7702
7910
|
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7703
|
-
sendRequestFunc:
|
|
7704
|
-
|
|
7911
|
+
sendRequestFunc: this.flushEventsWithOptOut.bind(this),
|
|
7912
|
+
queueStorage: this.queueStorage,
|
|
7913
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
7914
|
+
usePersistence: usePersistence,
|
|
7915
|
+
stopAllBatchingFunc: this.stopRecording.bind(this),
|
|
7916
|
+
|
|
7917
|
+
// increased throttle and shared lock timeout because recording events are very high frequency.
|
|
7918
|
+
// this will minimize the amount of lock contention between enqueued events.
|
|
7919
|
+
// for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
|
|
7920
|
+
enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
|
|
7921
|
+
sharedLockTimeoutMS: 10 * 1000,
|
|
7705
7922
|
});
|
|
7706
7923
|
};
|
|
7707
7924
|
|
|
7925
|
+
SessionRecording.prototype.unloadPersistedData = function () {
|
|
7926
|
+
this.batcher.stop();
|
|
7927
|
+
return this.batcher.flush()
|
|
7928
|
+
.then(function () {
|
|
7929
|
+
return this.queueStorage.removeItem(this.batcherKey);
|
|
7930
|
+
}.bind(this));
|
|
7931
|
+
};
|
|
7932
|
+
|
|
7708
7933
|
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7709
7934
|
return this._mixpanel.get_config(configVar);
|
|
7710
7935
|
};
|
|
@@ -7717,6 +7942,11 @@
|
|
|
7717
7942
|
};
|
|
7718
7943
|
|
|
7719
7944
|
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
7945
|
+
if (this._rrwebRecord === null) {
|
|
7946
|
+
this.reportError('rrweb record function not provided. ');
|
|
7947
|
+
return;
|
|
7948
|
+
}
|
|
7949
|
+
|
|
7720
7950
|
if (this._stopRecording !== null) {
|
|
7721
7951
|
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
7722
7952
|
return;
|
|
@@ -7728,15 +7958,21 @@
|
|
|
7728
7958
|
logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7729
7959
|
}
|
|
7730
7960
|
|
|
7961
|
+
if (!this.maxExpires) {
|
|
7962
|
+
this.maxExpires = new Date().getTime() + this.recordMaxMs;
|
|
7963
|
+
}
|
|
7964
|
+
|
|
7731
7965
|
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7732
7966
|
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7733
7967
|
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7734
7968
|
logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7735
7969
|
}
|
|
7736
7970
|
|
|
7737
|
-
this.replayStartTime
|
|
7738
|
-
|
|
7739
|
-
|
|
7971
|
+
if (!this.replayStartTime) {
|
|
7972
|
+
this.replayStartTime = new Date().getTime();
|
|
7973
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
7974
|
+
this.replayStartUrl = _.info.currentUrl();
|
|
7975
|
+
}
|
|
7740
7976
|
|
|
7741
7977
|
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7742
7978
|
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
@@ -7749,10 +7985,12 @@
|
|
|
7749
7985
|
this.batcher.start();
|
|
7750
7986
|
}
|
|
7751
7987
|
|
|
7752
|
-
var resetIdleTimeout =
|
|
7988
|
+
var resetIdleTimeout = function () {
|
|
7753
7989
|
clearTimeout(this.idleTimeoutId);
|
|
7754
|
-
|
|
7755
|
-
|
|
7990
|
+
var idleTimeoutMs = this.getConfig('record_idle_timeout_ms');
|
|
7991
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, idleTimeoutMs);
|
|
7992
|
+
this.idleExpires = new Date().getTime() + idleTimeoutMs;
|
|
7993
|
+
}.bind(this);
|
|
7756
7994
|
|
|
7757
7995
|
var blockSelector = this.getConfig('record_block_selector');
|
|
7758
7996
|
if (blockSelector === '' || blockSelector === null) {
|
|
@@ -7760,8 +7998,7 @@
|
|
|
7760
7998
|
}
|
|
7761
7999
|
|
|
7762
8000
|
this._stopRecording = this._rrwebRecord({
|
|
7763
|
-
'emit':
|
|
7764
|
-
this.batcher.enqueue(ev);
|
|
8001
|
+
'emit': addOptOutCheckMixpanelLib(function (ev) {
|
|
7765
8002
|
if (isUserEvent(ev)) {
|
|
7766
8003
|
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
7767
8004
|
// start flushing again after user activity
|
|
@@ -7769,7 +8006,10 @@
|
|
|
7769
8006
|
}
|
|
7770
8007
|
resetIdleTimeout();
|
|
7771
8008
|
}
|
|
7772
|
-
|
|
8009
|
+
|
|
8010
|
+
// promise only used to await during tests
|
|
8011
|
+
this.__enqueuePromise = this.batcher.enqueue(ev);
|
|
8012
|
+
}.bind(this)),
|
|
7773
8013
|
'blockClass': this.getConfig('record_block_class'),
|
|
7774
8014
|
'blockSelector': blockSelector,
|
|
7775
8015
|
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
@@ -7795,10 +8035,11 @@
|
|
|
7795
8035
|
|
|
7796
8036
|
resetIdleTimeout();
|
|
7797
8037
|
|
|
7798
|
-
|
|
8038
|
+
var maxTimeoutMs = this.maxExpires - new Date().getTime();
|
|
8039
|
+
this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
|
|
7799
8040
|
};
|
|
7800
8041
|
|
|
7801
|
-
SessionRecording.prototype.stopRecording = function () {
|
|
8042
|
+
SessionRecording.prototype.stopRecording = function (skipFlush) {
|
|
7802
8043
|
if (!this.isRrwebStopped()) {
|
|
7803
8044
|
try {
|
|
7804
8045
|
this._stopRecording();
|
|
@@ -7808,17 +8049,19 @@
|
|
|
7808
8049
|
this._stopRecording = null;
|
|
7809
8050
|
}
|
|
7810
8051
|
|
|
8052
|
+
var flushPromise;
|
|
7811
8053
|
if (this.batcher.stopped) {
|
|
7812
8054
|
// never got user activity to flush after reset, so just clear the batcher
|
|
7813
|
-
this.batcher.clear();
|
|
7814
|
-
} else {
|
|
8055
|
+
flushPromise = this.batcher.clear();
|
|
8056
|
+
} else if (!skipFlush) {
|
|
7815
8057
|
// flush any remaining events from running batcher
|
|
7816
|
-
this.batcher.flush();
|
|
7817
|
-
this.batcher.stop();
|
|
8058
|
+
flushPromise = this.batcher.flush();
|
|
7818
8059
|
}
|
|
8060
|
+
this.batcher.stop();
|
|
7819
8061
|
|
|
7820
8062
|
clearTimeout(this.idleTimeoutId);
|
|
7821
8063
|
clearTimeout(this.maxTimeoutId);
|
|
8064
|
+
return flushPromise;
|
|
7822
8065
|
};
|
|
7823
8066
|
|
|
7824
8067
|
SessionRecording.prototype.isRrwebStopped = function () {
|
|
@@ -7830,7 +8073,54 @@
|
|
|
7830
8073
|
* we stop recording and dump any queued events if the user has opted out.
|
|
7831
8074
|
*/
|
|
7832
8075
|
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
7833
|
-
this._flushEvents(data, options, cb,
|
|
8076
|
+
this._flushEvents(data, options, cb, this._onOptOut.bind(this));
|
|
8077
|
+
};
|
|
8078
|
+
|
|
8079
|
+
/**
|
|
8080
|
+
* @returns {SerializedRecording}
|
|
8081
|
+
*/
|
|
8082
|
+
SessionRecording.prototype.serialize = function () {
|
|
8083
|
+
// don't break if mixpanel instance was destroyed at some point
|
|
8084
|
+
var tabId;
|
|
8085
|
+
try {
|
|
8086
|
+
tabId = this._mixpanel.get_tab_id();
|
|
8087
|
+
} catch (e) {
|
|
8088
|
+
this.reportError('Error getting tab ID for serialization ', e);
|
|
8089
|
+
tabId = null;
|
|
8090
|
+
}
|
|
8091
|
+
|
|
8092
|
+
return {
|
|
8093
|
+
'replayId': this.replayId,
|
|
8094
|
+
'seqNo': this.seqNo,
|
|
8095
|
+
'replayStartTime': this.replayStartTime,
|
|
8096
|
+
'batchStartUrl': this.batchStartUrl,
|
|
8097
|
+
'replayStartUrl': this.replayStartUrl,
|
|
8098
|
+
'idleExpires': this.idleExpires,
|
|
8099
|
+
'maxExpires': this.maxExpires,
|
|
8100
|
+
'tabId': tabId,
|
|
8101
|
+
};
|
|
8102
|
+
};
|
|
8103
|
+
|
|
8104
|
+
|
|
8105
|
+
/**
|
|
8106
|
+
* @static
|
|
8107
|
+
* @param {SerializedRecording} serializedRecording
|
|
8108
|
+
* @param {SessionRecordingOptions} options
|
|
8109
|
+
* @returns {SessionRecording}
|
|
8110
|
+
*/
|
|
8111
|
+
SessionRecording.deserialize = function (serializedRecording, options) {
|
|
8112
|
+
var recording = new SessionRecording(_.extend({}, options, {
|
|
8113
|
+
replayId: serializedRecording['replayId'],
|
|
8114
|
+
batchStartUrl: serializedRecording['batchStartUrl'],
|
|
8115
|
+
replayStartUrl: serializedRecording['replayStartUrl'],
|
|
8116
|
+
idleExpires: serializedRecording['idleExpires'],
|
|
8117
|
+
maxExpires: serializedRecording['maxExpires'],
|
|
8118
|
+
replayStartTime: serializedRecording['replayStartTime'],
|
|
8119
|
+
seqNo: serializedRecording['seqNo'],
|
|
8120
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
8121
|
+
}));
|
|
8122
|
+
|
|
8123
|
+
return recording;
|
|
7834
8124
|
};
|
|
7835
8125
|
|
|
7836
8126
|
SessionRecording.prototype._onOptOut = function (code) {
|
|
@@ -7841,7 +8131,7 @@
|
|
|
7841
8131
|
};
|
|
7842
8132
|
|
|
7843
8133
|
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
7844
|
-
var onSuccess =
|
|
8134
|
+
var onSuccess = function (response, responseBody) {
|
|
7845
8135
|
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
7846
8136
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
7847
8137
|
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
@@ -7849,13 +8139,15 @@
|
|
|
7849
8139
|
this.seqNo++;
|
|
7850
8140
|
this.batchStartUrl = _.info.currentUrl();
|
|
7851
8141
|
}
|
|
8142
|
+
|
|
8143
|
+
this._onBatchSent();
|
|
7852
8144
|
callback({
|
|
7853
8145
|
status: 0,
|
|
7854
8146
|
httpStatusCode: response.status,
|
|
7855
8147
|
responseBody: responseBody,
|
|
7856
8148
|
retryAfter: response.headers.get('Retry-After')
|
|
7857
8149
|
});
|
|
7858
|
-
}
|
|
8150
|
+
}.bind(this);
|
|
7859
8151
|
|
|
7860
8152
|
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
7861
8153
|
'method': 'POST',
|
|
@@ -7876,7 +8168,7 @@
|
|
|
7876
8168
|
};
|
|
7877
8169
|
|
|
7878
8170
|
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
7879
|
-
|
|
8171
|
+
var numEvents = data.length;
|
|
7880
8172
|
|
|
7881
8173
|
if (numEvents > 0) {
|
|
7882
8174
|
var replayId = this.replayId;
|
|
@@ -7921,10 +8213,10 @@
|
|
|
7921
8213
|
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
7922
8214
|
new Response(gzipStream)
|
|
7923
8215
|
.blob()
|
|
7924
|
-
.then(
|
|
8216
|
+
.then(function(compressedBlob) {
|
|
7925
8217
|
reqParams['format'] = 'gzip';
|
|
7926
8218
|
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
7927
|
-
}
|
|
8219
|
+
}.bind(this));
|
|
7928
8220
|
} else {
|
|
7929
8221
|
reqParams['format'] = 'body';
|
|
7930
8222
|
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
@@ -7945,54 +8237,208 @@
|
|
|
7945
8237
|
}
|
|
7946
8238
|
};
|
|
7947
8239
|
|
|
8240
|
+
/**
|
|
8241
|
+
* Module for handling the storage and retrieval of recording metadata as well as any active recordings.
|
|
8242
|
+
* Makes sure that only one tab can be recording at a time.
|
|
8243
|
+
*/
|
|
8244
|
+
var RecordingRegistry = function (options) {
|
|
8245
|
+
this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
8246
|
+
this.errorReporter = options.errorReporter;
|
|
8247
|
+
this.mixpanelInstance = options.mixpanelInstance;
|
|
8248
|
+
this.sharedLockStorage = options.sharedLockStorage;
|
|
8249
|
+
};
|
|
8250
|
+
|
|
8251
|
+
RecordingRegistry.prototype.handleError = function (err) {
|
|
8252
|
+
this.errorReporter('IndexedDB error: ', err);
|
|
8253
|
+
};
|
|
8254
|
+
|
|
8255
|
+
/**
|
|
8256
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
8257
|
+
*/
|
|
8258
|
+
RecordingRegistry.prototype.setActiveRecording = function (serializedRecording) {
|
|
8259
|
+
var tabId = serializedRecording['tabId'];
|
|
8260
|
+
if (!tabId) {
|
|
8261
|
+
console.warn('No tab ID is set, cannot persist recording metadata.');
|
|
8262
|
+
return PromisePolyfill.resolve();
|
|
8263
|
+
}
|
|
8264
|
+
|
|
8265
|
+
return this.idb.init()
|
|
8266
|
+
.then(function () {
|
|
8267
|
+
return this.idb.setItem(tabId, serializedRecording);
|
|
8268
|
+
}.bind(this))
|
|
8269
|
+
.catch(this.handleError.bind(this));
|
|
8270
|
+
};
|
|
8271
|
+
|
|
8272
|
+
/**
|
|
8273
|
+
* @returns {Promise<import('./session-recording').SerializedRecording>}
|
|
8274
|
+
*/
|
|
8275
|
+
RecordingRegistry.prototype.getActiveRecording = function () {
|
|
8276
|
+
return this.idb.init()
|
|
8277
|
+
.then(function () {
|
|
8278
|
+
return this.idb.getItem(this.mixpanelInstance.get_tab_id());
|
|
8279
|
+
}.bind(this))
|
|
8280
|
+
.then(function (serializedRecording) {
|
|
8281
|
+
return isRecordingExpired(serializedRecording) ? null : serializedRecording;
|
|
8282
|
+
}.bind(this))
|
|
8283
|
+
.catch(this.handleError.bind(this));
|
|
8284
|
+
};
|
|
8285
|
+
|
|
8286
|
+
RecordingRegistry.prototype.clearActiveRecording = function () {
|
|
8287
|
+
// mark recording as expired instead of deleting it in case the page unloads mid-flush and doesn't make it to ingestion.
|
|
8288
|
+
// this will ensure the next pageload will flush the remaining events, but not try to continue the recording.
|
|
8289
|
+
return this.getActiveRecording()
|
|
8290
|
+
.then(function (serializedRecording) {
|
|
8291
|
+
if (serializedRecording) {
|
|
8292
|
+
serializedRecording['maxExpires'] = 0;
|
|
8293
|
+
return this.setActiveRecording(serializedRecording);
|
|
8294
|
+
}
|
|
8295
|
+
}.bind(this))
|
|
8296
|
+
.catch(this.handleError.bind(this));
|
|
8297
|
+
};
|
|
8298
|
+
|
|
8299
|
+
/**
|
|
8300
|
+
* Flush any inactive recordings from the registry to minimize data loss.
|
|
8301
|
+
* The main idea here is that we can flush remaining rrweb events on the next page load if a tab is closed mid-batch.
|
|
8302
|
+
*/
|
|
8303
|
+
RecordingRegistry.prototype.flushInactiveRecordings = function () {
|
|
8304
|
+
return this.idb.init()
|
|
8305
|
+
.then(function() {
|
|
8306
|
+
return this.idb.getAll();
|
|
8307
|
+
}.bind(this))
|
|
8308
|
+
.then(function (serializedRecordings) {
|
|
8309
|
+
// clean up any expired recordings from the registry, non-expired ones may be active in other tabs
|
|
8310
|
+
var unloadPromises = serializedRecordings
|
|
8311
|
+
.filter(function (serializedRecording) {
|
|
8312
|
+
return isRecordingExpired(serializedRecording);
|
|
8313
|
+
})
|
|
8314
|
+
.map(function (serializedRecording) {
|
|
8315
|
+
var sessionRecording = SessionRecording.deserialize(serializedRecording, {
|
|
8316
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8317
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8318
|
+
});
|
|
8319
|
+
return sessionRecording.unloadPersistedData()
|
|
8320
|
+
.then(function () {
|
|
8321
|
+
// expired recording was successfully flushed, we can clean it up from the registry
|
|
8322
|
+
return this.idb.removeItem(serializedRecording['tabId']);
|
|
8323
|
+
}.bind(this))
|
|
8324
|
+
.catch(this.handleError.bind(this));
|
|
8325
|
+
}.bind(this));
|
|
8326
|
+
|
|
8327
|
+
return PromisePolyfill.all(unloadPromises);
|
|
8328
|
+
}.bind(this))
|
|
8329
|
+
.catch(this.handleError.bind(this));
|
|
8330
|
+
};
|
|
8331
|
+
|
|
7948
8332
|
var logger = console_with_prefix('recorder');
|
|
7949
8333
|
|
|
7950
8334
|
/**
|
|
7951
|
-
* Recorder API:
|
|
8335
|
+
* Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
|
|
7952
8336
|
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
7953
|
-
|
|
7954
|
-
var MixpanelRecorder = function(mixpanelInstance) {
|
|
7955
|
-
this.
|
|
8337
|
+
*/
|
|
8338
|
+
var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage) {
|
|
8339
|
+
this.mixpanelInstance = mixpanelInstance;
|
|
8340
|
+
this.rrwebRecord = rrwebRecord || record;
|
|
8341
|
+
this.sharedLockStorage = sharedLockStorage;
|
|
8342
|
+
|
|
8343
|
+
/**
|
|
8344
|
+
* @member {import('./registry').RecordingRegistry}
|
|
8345
|
+
*/
|
|
8346
|
+
this.recordingRegistry = new RecordingRegistry({
|
|
8347
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8348
|
+
errorReporter: logger.error,
|
|
8349
|
+
sharedLockStorage: sharedLockStorage
|
|
8350
|
+
});
|
|
8351
|
+
this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
|
|
8352
|
+
|
|
7956
8353
|
this.activeRecording = null;
|
|
7957
8354
|
};
|
|
7958
8355
|
|
|
7959
|
-
MixpanelRecorder.prototype.startRecording = function(
|
|
8356
|
+
MixpanelRecorder.prototype.startRecording = function(options) {
|
|
8357
|
+
options = options || {};
|
|
7960
8358
|
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
7961
8359
|
logger.log('Recording already in progress, skipping startRecording.');
|
|
7962
8360
|
return;
|
|
7963
8361
|
}
|
|
7964
8362
|
|
|
7965
|
-
var onIdleTimeout =
|
|
8363
|
+
var onIdleTimeout = function () {
|
|
7966
8364
|
logger.log('Idle timeout reached, restarting recording.');
|
|
7967
8365
|
this.resetRecording();
|
|
7968
|
-
}
|
|
8366
|
+
}.bind(this);
|
|
7969
8367
|
|
|
7970
|
-
var onMaxLengthReached =
|
|
8368
|
+
var onMaxLengthReached = function () {
|
|
7971
8369
|
logger.log('Max recording length reached, stopping recording.');
|
|
7972
8370
|
this.resetRecording();
|
|
7973
|
-
}
|
|
8371
|
+
}.bind(this);
|
|
7974
8372
|
|
|
7975
|
-
|
|
7976
|
-
|
|
8373
|
+
var onBatchSent = function () {
|
|
8374
|
+
this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8375
|
+
this['__flushPromise'] = this.activeRecording.batcher._flushPromise;
|
|
8376
|
+
}.bind(this);
|
|
8377
|
+
|
|
8378
|
+
/**
|
|
8379
|
+
* @type {import('./session-recording').SessionRecordingOptions}
|
|
8380
|
+
*/
|
|
8381
|
+
var sessionRecordingOptions = {
|
|
8382
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8383
|
+
onBatchSent: onBatchSent,
|
|
7977
8384
|
onIdleTimeout: onIdleTimeout,
|
|
7978
8385
|
onMaxLengthReached: onMaxLengthReached,
|
|
7979
8386
|
replayId: _.UUID(),
|
|
7980
|
-
rrwebRecord:
|
|
7981
|
-
|
|
8387
|
+
rrwebRecord: this.rrwebRecord,
|
|
8388
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8389
|
+
};
|
|
8390
|
+
|
|
8391
|
+
if (options.activeSerializedRecording) {
|
|
8392
|
+
this.activeRecording = SessionRecording.deserialize(options.activeSerializedRecording, sessionRecordingOptions);
|
|
8393
|
+
} else {
|
|
8394
|
+
this.activeRecording = new SessionRecording(sessionRecordingOptions);
|
|
8395
|
+
}
|
|
7982
8396
|
|
|
7983
|
-
this.activeRecording.startRecording(shouldStopBatcher);
|
|
8397
|
+
this.activeRecording.startRecording(options.shouldStopBatcher);
|
|
8398
|
+
return this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
7984
8399
|
};
|
|
7985
8400
|
|
|
7986
8401
|
MixpanelRecorder.prototype.stopRecording = function() {
|
|
8402
|
+
var stopPromise = this._stopCurrentRecording(false);
|
|
8403
|
+
this.recordingRegistry.clearActiveRecording();
|
|
8404
|
+
this.activeRecording = null;
|
|
8405
|
+
return stopPromise;
|
|
8406
|
+
};
|
|
8407
|
+
|
|
8408
|
+
MixpanelRecorder.prototype.pauseRecording = function() {
|
|
8409
|
+
return this._stopCurrentRecording(false);
|
|
8410
|
+
};
|
|
8411
|
+
|
|
8412
|
+
MixpanelRecorder.prototype._stopCurrentRecording = function(skipFlush) {
|
|
7987
8413
|
if (this.activeRecording) {
|
|
7988
|
-
this.activeRecording.stopRecording();
|
|
7989
|
-
|
|
8414
|
+
return this.activeRecording.stopRecording(skipFlush);
|
|
8415
|
+
}
|
|
8416
|
+
return PromisePolyfill.resolve();
|
|
8417
|
+
};
|
|
8418
|
+
|
|
8419
|
+
MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
|
|
8420
|
+
if (this.activeRecording && this.activeRecording.isRrwebStopped()) {
|
|
8421
|
+
this.activeRecording.startRecording(false);
|
|
8422
|
+
return PromisePolyfill.resolve(null);
|
|
7990
8423
|
}
|
|
8424
|
+
|
|
8425
|
+
return this.recordingRegistry.getActiveRecording()
|
|
8426
|
+
.then(function (activeSerializedRecording) {
|
|
8427
|
+
if (activeSerializedRecording) {
|
|
8428
|
+
return this.startRecording({activeSerializedRecording: activeSerializedRecording});
|
|
8429
|
+
} else if (startNewIfInactive) {
|
|
8430
|
+
return this.startRecording({shouldStopBatcher: false});
|
|
8431
|
+
} else {
|
|
8432
|
+
logger.log('No resumable recording found.');
|
|
8433
|
+
return null;
|
|
8434
|
+
}
|
|
8435
|
+
}.bind(this));
|
|
7991
8436
|
};
|
|
7992
8437
|
|
|
8438
|
+
|
|
7993
8439
|
MixpanelRecorder.prototype.resetRecording = function () {
|
|
7994
8440
|
this.stopRecording();
|
|
7995
|
-
this.startRecording(true);
|
|
8441
|
+
this.startRecording({shouldStopBatcher: true});
|
|
7996
8442
|
};
|
|
7997
8443
|
|
|
7998
8444
|
MixpanelRecorder.prototype.getActiveReplayId = function () {
|