mixpanel-browser 2.60.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/README.md +2 -2
- package/dist/mixpanel-core.cjs.js +398 -128
- 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 +398 -128
- package/dist/mixpanel.amd.js +786 -242
- package/dist/mixpanel.cjs.js +786 -242
- package/dist/mixpanel.globals.js +398 -128
- package/dist/mixpanel.min.js +143 -138
- package/dist/mixpanel.module.js +786 -242
- package/dist/mixpanel.umd.js +786 -242
- package/package.json +2 -1
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +119 -19
- 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
package/dist/mixpanel.module.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
2
|
+
var win;
|
|
3
|
+
if (typeof(window) === 'undefined') {
|
|
4
|
+
var loc = {
|
|
5
|
+
hostname: ''
|
|
6
|
+
};
|
|
7
|
+
win = {
|
|
8
|
+
navigator: { userAgent: '', onLine: true },
|
|
9
|
+
document: {
|
|
10
|
+
createElement: function() { return {}; },
|
|
11
|
+
location: loc,
|
|
12
|
+
referrer: ''
|
|
13
|
+
},
|
|
14
|
+
screen: { width: 0, height: 0 },
|
|
15
|
+
location: loc,
|
|
16
|
+
addEventListener: function() {},
|
|
17
|
+
removeEventListener: function() {}
|
|
18
|
+
};
|
|
19
|
+
} else {
|
|
20
|
+
win = window;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
var NodeType;
|
|
2
24
|
(function (NodeType) {
|
|
3
25
|
NodeType[NodeType["Document"] = 0] = "Document";
|
|
@@ -4474,64 +4496,6 @@ record.takeFullSnapshot = (isCheckout) => {
|
|
|
4474
4496
|
};
|
|
4475
4497
|
record.mirror = mirror;
|
|
4476
4498
|
|
|
4477
|
-
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
4478
|
-
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
|
|
4479
|
-
EventType2[EventType2["Load"] = 1] = "Load";
|
|
4480
|
-
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
|
|
4481
|
-
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
|
|
4482
|
-
EventType2[EventType2["Meta"] = 4] = "Meta";
|
|
4483
|
-
EventType2[EventType2["Custom"] = 5] = "Custom";
|
|
4484
|
-
EventType2[EventType2["Plugin"] = 6] = "Plugin";
|
|
4485
|
-
return EventType2;
|
|
4486
|
-
})(EventType || {});
|
|
4487
|
-
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
4488
|
-
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
|
|
4489
|
-
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
|
|
4490
|
-
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
|
|
4491
|
-
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
|
|
4492
|
-
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
|
|
4493
|
-
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
|
|
4494
|
-
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
|
|
4495
|
-
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
|
|
4496
|
-
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
|
|
4497
|
-
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
|
|
4498
|
-
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
|
|
4499
|
-
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
|
|
4500
|
-
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
|
|
4501
|
-
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
|
|
4502
|
-
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
|
|
4503
|
-
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
|
|
4504
|
-
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
|
|
4505
|
-
return IncrementalSource2;
|
|
4506
|
-
})(IncrementalSource || {});
|
|
4507
|
-
|
|
4508
|
-
var Config = {
|
|
4509
|
-
DEBUG: false,
|
|
4510
|
-
LIB_VERSION: '2.60.0'
|
|
4511
|
-
};
|
|
4512
|
-
|
|
4513
|
-
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
4514
|
-
var win;
|
|
4515
|
-
if (typeof(window) === 'undefined') {
|
|
4516
|
-
var loc = {
|
|
4517
|
-
hostname: ''
|
|
4518
|
-
};
|
|
4519
|
-
win = {
|
|
4520
|
-
navigator: { userAgent: '', onLine: true },
|
|
4521
|
-
document: {
|
|
4522
|
-
createElement: function() { return {}; },
|
|
4523
|
-
location: loc,
|
|
4524
|
-
referrer: ''
|
|
4525
|
-
},
|
|
4526
|
-
screen: { width: 0, height: 0 },
|
|
4527
|
-
location: loc,
|
|
4528
|
-
addEventListener: function() {},
|
|
4529
|
-
removeEventListener: function() {}
|
|
4530
|
-
};
|
|
4531
|
-
} else {
|
|
4532
|
-
win = window;
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
4499
|
var setImmediate = win['setImmediate'];
|
|
4536
4500
|
var builtInProp, cycle, schedulingQueue,
|
|
4537
4501
|
ToString = Object.prototype.toString,
|
|
@@ -4894,6 +4858,42 @@ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]'
|
|
|
4894
4858
|
PromisePolyfill = NpoPromise;
|
|
4895
4859
|
}
|
|
4896
4860
|
|
|
4861
|
+
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
4862
|
+
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
|
|
4863
|
+
EventType2[EventType2["Load"] = 1] = "Load";
|
|
4864
|
+
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
|
|
4865
|
+
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
|
|
4866
|
+
EventType2[EventType2["Meta"] = 4] = "Meta";
|
|
4867
|
+
EventType2[EventType2["Custom"] = 5] = "Custom";
|
|
4868
|
+
EventType2[EventType2["Plugin"] = 6] = "Plugin";
|
|
4869
|
+
return EventType2;
|
|
4870
|
+
})(EventType || {});
|
|
4871
|
+
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
4872
|
+
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
|
|
4873
|
+
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
|
|
4874
|
+
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
|
|
4875
|
+
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
|
|
4876
|
+
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
|
|
4877
|
+
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
|
|
4878
|
+
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
|
|
4879
|
+
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
|
|
4880
|
+
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
|
|
4881
|
+
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
|
|
4882
|
+
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
|
|
4883
|
+
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
|
|
4884
|
+
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
|
|
4885
|
+
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
|
|
4886
|
+
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
|
|
4887
|
+
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
|
|
4888
|
+
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
|
|
4889
|
+
return IncrementalSource2;
|
|
4890
|
+
})(IncrementalSource || {});
|
|
4891
|
+
|
|
4892
|
+
var Config = {
|
|
4893
|
+
DEBUG: false,
|
|
4894
|
+
LIB_VERSION: '2.61.0'
|
|
4895
|
+
};
|
|
4896
|
+
|
|
4897
4897
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
4898
4898
|
|
|
4899
4899
|
// Maximum allowed session recording length
|
|
@@ -5973,15 +5973,9 @@ _.cookie = {
|
|
|
5973
5973
|
}
|
|
5974
5974
|
};
|
|
5975
5975
|
|
|
5976
|
-
var
|
|
5977
|
-
var localStorageSupported = function(storage, forceCheck) {
|
|
5978
|
-
if (_localStorageSupported !== null && !forceCheck) {
|
|
5979
|
-
return _localStorageSupported;
|
|
5980
|
-
}
|
|
5981
|
-
|
|
5976
|
+
var _testStorageSupported = function (storage) {
|
|
5982
5977
|
var supported = true;
|
|
5983
5978
|
try {
|
|
5984
|
-
storage = storage || win.localStorage;
|
|
5985
5979
|
var key = '__mplss_' + cheap_guid(8),
|
|
5986
5980
|
val = 'xyz';
|
|
5987
5981
|
storage.setItem(key, val);
|
|
@@ -5992,59 +5986,74 @@ var localStorageSupported = function(storage, forceCheck) {
|
|
|
5992
5986
|
} catch (err) {
|
|
5993
5987
|
supported = false;
|
|
5994
5988
|
}
|
|
5995
|
-
|
|
5996
|
-
_localStorageSupported = supported;
|
|
5997
5989
|
return supported;
|
|
5998
5990
|
};
|
|
5999
5991
|
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
return supported;
|
|
6008
|
-
},
|
|
5992
|
+
var _localStorageSupported = null;
|
|
5993
|
+
var localStorageSupported = function(storage, forceCheck) {
|
|
5994
|
+
if (_localStorageSupported !== null && !forceCheck) {
|
|
5995
|
+
return _localStorageSupported;
|
|
5996
|
+
}
|
|
5997
|
+
return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
|
|
5998
|
+
};
|
|
6009
5999
|
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6000
|
+
var _sessionStorageSupported = null;
|
|
6001
|
+
var sessionStorageSupported = function(storage, forceCheck) {
|
|
6002
|
+
if (_sessionStorageSupported !== null && !forceCheck) {
|
|
6003
|
+
return _sessionStorageSupported;
|
|
6004
|
+
}
|
|
6005
|
+
return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
|
|
6006
|
+
};
|
|
6013
6007
|
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
_.localStorage.error(err);
|
|
6019
|
-
}
|
|
6020
|
-
return null;
|
|
6021
|
-
},
|
|
6008
|
+
function _storageWrapper(storage, name, is_supported_fn) {
|
|
6009
|
+
var log_error = function(msg) {
|
|
6010
|
+
console$1.error(name + ' error: ' + msg);
|
|
6011
|
+
};
|
|
6022
6012
|
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6013
|
+
return {
|
|
6014
|
+
is_supported: function(forceCheck) {
|
|
6015
|
+
var supported = is_supported_fn(storage, forceCheck);
|
|
6016
|
+
if (!supported) {
|
|
6017
|
+
console$1.error(name + ' unsupported');
|
|
6018
|
+
}
|
|
6019
|
+
return supported;
|
|
6020
|
+
},
|
|
6021
|
+
error: log_error,
|
|
6022
|
+
get: function(key) {
|
|
6023
|
+
try {
|
|
6024
|
+
return storage.getItem(key);
|
|
6025
|
+
} catch (err) {
|
|
6026
|
+
log_error(err);
|
|
6027
|
+
}
|
|
6028
|
+
return null;
|
|
6029
|
+
},
|
|
6030
|
+
parse: function(key) {
|
|
6031
|
+
try {
|
|
6032
|
+
return _.JSONDecode(storage.getItem(key)) || {};
|
|
6033
|
+
} catch (err) {
|
|
6034
|
+
// noop
|
|
6035
|
+
}
|
|
6036
|
+
return null;
|
|
6037
|
+
},
|
|
6038
|
+
set: function(key, value) {
|
|
6039
|
+
try {
|
|
6040
|
+
storage.setItem(key, value);
|
|
6041
|
+
} catch (err) {
|
|
6042
|
+
log_error(err);
|
|
6043
|
+
}
|
|
6044
|
+
},
|
|
6045
|
+
remove: function(key) {
|
|
6046
|
+
try {
|
|
6047
|
+
storage.removeItem(key);
|
|
6048
|
+
} catch (err) {
|
|
6049
|
+
log_error(err);
|
|
6050
|
+
}
|
|
6028
6051
|
}
|
|
6029
|
-
|
|
6030
|
-
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6031
6054
|
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
win.localStorage.setItem(name, value);
|
|
6035
|
-
} catch (err) {
|
|
6036
|
-
_.localStorage.error(err);
|
|
6037
|
-
}
|
|
6038
|
-
},
|
|
6039
|
-
|
|
6040
|
-
remove: function(name) {
|
|
6041
|
-
try {
|
|
6042
|
-
win.localStorage.removeItem(name);
|
|
6043
|
-
} catch (err) {
|
|
6044
|
-
_.localStorage.error(err);
|
|
6045
|
-
}
|
|
6046
|
-
}
|
|
6047
|
-
};
|
|
6055
|
+
_.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
|
|
6056
|
+
_.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
|
|
6048
6057
|
|
|
6049
6058
|
_.register_event = (function() {
|
|
6050
6059
|
// written by Dean Edwards, 2005
|
|
@@ -6571,6 +6580,31 @@ _.info = {
|
|
|
6571
6580
|
}
|
|
6572
6581
|
};
|
|
6573
6582
|
|
|
6583
|
+
/**
|
|
6584
|
+
* Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
|
|
6585
|
+
* Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
|
|
6586
|
+
*/
|
|
6587
|
+
var batchedThrottle = function (fn, waitMs) {
|
|
6588
|
+
var timeoutPromise = null;
|
|
6589
|
+
var throttledItems = [];
|
|
6590
|
+
return function (item) {
|
|
6591
|
+
var self = this;
|
|
6592
|
+
throttledItems.push(item);
|
|
6593
|
+
|
|
6594
|
+
if (!timeoutPromise) {
|
|
6595
|
+
timeoutPromise = new PromisePolyfill(function (resolve) {
|
|
6596
|
+
setTimeout(function () {
|
|
6597
|
+
var returnValue = fn.apply(self, [throttledItems]);
|
|
6598
|
+
timeoutPromise = null;
|
|
6599
|
+
throttledItems = [];
|
|
6600
|
+
resolve(returnValue);
|
|
6601
|
+
}, waitMs);
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6604
|
+
return timeoutPromise;
|
|
6605
|
+
};
|
|
6606
|
+
};
|
|
6607
|
+
|
|
6574
6608
|
var cheap_guid = function(maxlen) {
|
|
6575
6609
|
var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
|
|
6576
6610
|
return maxlen ? guid.substring(0, maxlen) : guid;
|
|
@@ -6613,6 +6647,8 @@ var isOnline = function() {
|
|
|
6613
6647
|
return _.isUndefined(onLine) || onLine;
|
|
6614
6648
|
};
|
|
6615
6649
|
|
|
6650
|
+
var NOOP_FUNC = function () {};
|
|
6651
|
+
|
|
6616
6652
|
var JSONStringify = null, JSONParse = null;
|
|
6617
6653
|
if (typeof JSON !== 'undefined') {
|
|
6618
6654
|
JSONStringify = JSON.stringify;
|
|
@@ -6621,20 +6657,143 @@ if (typeof JSON !== 'undefined') {
|
|
|
6621
6657
|
JSONStringify = JSONStringify || _.JSONEncode;
|
|
6622
6658
|
JSONParse = JSONParse || _.JSONDecode;
|
|
6623
6659
|
|
|
6624
|
-
// EXPORTS (for closure compiler)
|
|
6625
|
-
_['toArray'] = _.toArray;
|
|
6626
|
-
_['isObject'] = _.isObject;
|
|
6627
|
-
_['JSONEncode'] = _.JSONEncode;
|
|
6628
|
-
_['JSONDecode'] = _.JSONDecode;
|
|
6629
|
-
_['isBlockedUA'] = _.isBlockedUA;
|
|
6630
|
-
_['isEmptyObject'] = _.isEmptyObject;
|
|
6660
|
+
// UNMINIFIED EXPORTS (for closure compiler)
|
|
6631
6661
|
_['info'] = _.info;
|
|
6632
|
-
_['info']['device'] = _.info.device;
|
|
6633
6662
|
_['info']['browser'] = _.info.browser;
|
|
6634
6663
|
_['info']['browserVersion'] = _.info.browserVersion;
|
|
6664
|
+
_['info']['device'] = _.info.device;
|
|
6635
6665
|
_['info']['properties'] = _.info.properties;
|
|
6666
|
+
_['isBlockedUA'] = _.isBlockedUA;
|
|
6667
|
+
_['isEmptyObject'] = _.isEmptyObject;
|
|
6668
|
+
_['isObject'] = _.isObject;
|
|
6669
|
+
_['JSONDecode'] = _.JSONDecode;
|
|
6670
|
+
_['JSONEncode'] = _.JSONEncode;
|
|
6671
|
+
_['toArray'] = _.toArray;
|
|
6636
6672
|
_['NPO'] = NpoPromise;
|
|
6637
6673
|
|
|
6674
|
+
var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
|
|
6675
|
+
|
|
6676
|
+
var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
|
|
6677
|
+
var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
|
|
6678
|
+
|
|
6679
|
+
// note: increment the version number when adding new object stores
|
|
6680
|
+
var DB_VERSION = 1;
|
|
6681
|
+
var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
|
|
6682
|
+
|
|
6683
|
+
/**
|
|
6684
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
6685
|
+
*/
|
|
6686
|
+
var IDBStorageWrapper = function (storeName) {
|
|
6687
|
+
/**
|
|
6688
|
+
* @type {Promise<IDBDatabase>|null}
|
|
6689
|
+
*/
|
|
6690
|
+
this.dbPromise = null;
|
|
6691
|
+
this.storeName = storeName;
|
|
6692
|
+
};
|
|
6693
|
+
|
|
6694
|
+
IDBStorageWrapper.prototype._openDb = function () {
|
|
6695
|
+
return new PromisePolyfill(function (resolve, reject) {
|
|
6696
|
+
var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
|
|
6697
|
+
openRequest['onerror'] = function () {
|
|
6698
|
+
reject(openRequest.error);
|
|
6699
|
+
};
|
|
6700
|
+
|
|
6701
|
+
openRequest['onsuccess'] = function () {
|
|
6702
|
+
resolve(openRequest.result);
|
|
6703
|
+
};
|
|
6704
|
+
|
|
6705
|
+
openRequest['onupgradeneeded'] = function (ev) {
|
|
6706
|
+
var db = ev.target.result;
|
|
6707
|
+
|
|
6708
|
+
OBJECT_STORES.forEach(function (storeName) {
|
|
6709
|
+
db.createObjectStore(storeName);
|
|
6710
|
+
});
|
|
6711
|
+
};
|
|
6712
|
+
});
|
|
6713
|
+
};
|
|
6714
|
+
|
|
6715
|
+
IDBStorageWrapper.prototype.init = function () {
|
|
6716
|
+
if (!win.indexedDB) {
|
|
6717
|
+
return PromisePolyfill.reject('indexedDB is not supported in this browser');
|
|
6718
|
+
}
|
|
6719
|
+
|
|
6720
|
+
if (!this.dbPromise) {
|
|
6721
|
+
this.dbPromise = this._openDb();
|
|
6722
|
+
}
|
|
6723
|
+
|
|
6724
|
+
return this.dbPromise
|
|
6725
|
+
.then(function (dbOrError) {
|
|
6726
|
+
if (dbOrError instanceof win['IDBDatabase']) {
|
|
6727
|
+
return PromisePolyfill.resolve();
|
|
6728
|
+
} else {
|
|
6729
|
+
return PromisePolyfill.reject(dbOrError);
|
|
6730
|
+
}
|
|
6731
|
+
});
|
|
6732
|
+
};
|
|
6733
|
+
|
|
6734
|
+
/**
|
|
6735
|
+
* @param {IDBTransactionMode} mode
|
|
6736
|
+
* @param {function(IDBObjectStore): void} storeCb
|
|
6737
|
+
*/
|
|
6738
|
+
IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
|
|
6739
|
+
var storeName = this.storeName;
|
|
6740
|
+
var doTransaction = function (db) {
|
|
6741
|
+
return new PromisePolyfill(function (resolve, reject) {
|
|
6742
|
+
var transaction = db.transaction(storeName, mode);
|
|
6743
|
+
transaction.oncomplete = function () {
|
|
6744
|
+
resolve(transaction);
|
|
6745
|
+
};
|
|
6746
|
+
transaction.onabort = transaction.onerror = function () {
|
|
6747
|
+
reject(transaction.error);
|
|
6748
|
+
};
|
|
6749
|
+
|
|
6750
|
+
storeCb(transaction.objectStore(storeName));
|
|
6751
|
+
});
|
|
6752
|
+
};
|
|
6753
|
+
|
|
6754
|
+
return this.dbPromise
|
|
6755
|
+
.then(doTransaction)
|
|
6756
|
+
.catch(function (err) {
|
|
6757
|
+
if (err['name'] === 'InvalidStateError') {
|
|
6758
|
+
// try reopening the DB if the connection is closed
|
|
6759
|
+
this.dbPromise = this._openDb();
|
|
6760
|
+
return this.dbPromise.then(doTransaction);
|
|
6761
|
+
} else {
|
|
6762
|
+
return PromisePolyfill.reject(err);
|
|
6763
|
+
}
|
|
6764
|
+
}.bind(this));
|
|
6765
|
+
};
|
|
6766
|
+
|
|
6767
|
+
IDBStorageWrapper.prototype.setItem = function (key, value) {
|
|
6768
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
6769
|
+
objectStore.put(value, key);
|
|
6770
|
+
});
|
|
6771
|
+
};
|
|
6772
|
+
|
|
6773
|
+
IDBStorageWrapper.prototype.getItem = function (key) {
|
|
6774
|
+
var req;
|
|
6775
|
+
return this.makeTransaction('readonly', function (objectStore) {
|
|
6776
|
+
req = objectStore.get(key);
|
|
6777
|
+
}).then(function () {
|
|
6778
|
+
return req.result;
|
|
6779
|
+
});
|
|
6780
|
+
};
|
|
6781
|
+
|
|
6782
|
+
IDBStorageWrapper.prototype.removeItem = function (key) {
|
|
6783
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
6784
|
+
objectStore.delete(key);
|
|
6785
|
+
});
|
|
6786
|
+
};
|
|
6787
|
+
|
|
6788
|
+
IDBStorageWrapper.prototype.getAll = function () {
|
|
6789
|
+
var req;
|
|
6790
|
+
return this.makeTransaction('readonly', function (objectStore) {
|
|
6791
|
+
req = objectStore.getAll();
|
|
6792
|
+
}).then(function () {
|
|
6793
|
+
return req.result;
|
|
6794
|
+
});
|
|
6795
|
+
};
|
|
6796
|
+
|
|
6638
6797
|
/**
|
|
6639
6798
|
* GDPR utils
|
|
6640
6799
|
*
|
|
@@ -6960,7 +7119,7 @@ var SharedLock = function(key, options) {
|
|
|
6960
7119
|
options = options || {};
|
|
6961
7120
|
|
|
6962
7121
|
this.storageKey = key;
|
|
6963
|
-
this.storage = options.storage ||
|
|
7122
|
+
this.storage = options.storage || win.localStorage;
|
|
6964
7123
|
this.pollIntervalMS = options.pollIntervalMS || 100;
|
|
6965
7124
|
this.timeoutMS = options.timeoutMS || 2000;
|
|
6966
7125
|
|
|
@@ -6975,7 +7134,6 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
|
|
|
6975
7134
|
return new Promise(_.bind(function (resolve, reject) {
|
|
6976
7135
|
var i = pid || (new Date().getTime() + '|' + Math.random());
|
|
6977
7136
|
var startTime = new Date().getTime();
|
|
6978
|
-
|
|
6979
7137
|
var key = this.storageKey;
|
|
6980
7138
|
var pollIntervalMS = this.pollIntervalMS;
|
|
6981
7139
|
var timeoutMS = this.timeoutMS;
|
|
@@ -7086,11 +7244,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
|
|
|
7086
7244
|
};
|
|
7087
7245
|
|
|
7088
7246
|
/**
|
|
7089
|
-
* @
|
|
7090
|
-
*/
|
|
7091
|
-
|
|
7092
|
-
/**
|
|
7093
|
-
* @type {StorageWrapper}
|
|
7247
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
7094
7248
|
*/
|
|
7095
7249
|
var LocalStorageWrapper = function (storageOverride) {
|
|
7096
7250
|
this.storage = storageOverride || localStorage;
|
|
@@ -7103,7 +7257,7 @@ LocalStorageWrapper.prototype.init = function () {
|
|
|
7103
7257
|
LocalStorageWrapper.prototype.setItem = function (key, value) {
|
|
7104
7258
|
return new PromisePolyfill(_.bind(function (resolve, reject) {
|
|
7105
7259
|
try {
|
|
7106
|
-
this.storage.setItem(key, value);
|
|
7260
|
+
this.storage.setItem(key, JSONStringify(value));
|
|
7107
7261
|
} catch (e) {
|
|
7108
7262
|
reject(e);
|
|
7109
7263
|
}
|
|
@@ -7115,7 +7269,7 @@ LocalStorageWrapper.prototype.getItem = function (key) {
|
|
|
7115
7269
|
return new PromisePolyfill(_.bind(function (resolve, reject) {
|
|
7116
7270
|
var item;
|
|
7117
7271
|
try {
|
|
7118
|
-
item = this.storage.getItem(key);
|
|
7272
|
+
item = JSONParse(this.storage.getItem(key));
|
|
7119
7273
|
} catch (e) {
|
|
7120
7274
|
reject(e);
|
|
7121
7275
|
}
|
|
@@ -7158,8 +7312,10 @@ var RequestQueue = function (storageKey, options) {
|
|
|
7158
7312
|
this.usePersistence = options.usePersistence;
|
|
7159
7313
|
if (this.usePersistence) {
|
|
7160
7314
|
this.queueStorage = options.queueStorage || new LocalStorageWrapper();
|
|
7161
|
-
this.lock = new SharedLock(storageKey, {
|
|
7162
|
-
|
|
7315
|
+
this.lock = new SharedLock(storageKey, {
|
|
7316
|
+
storage: options.sharedLockStorage || win.localStorage,
|
|
7317
|
+
timeoutMS: options.sharedLockTimeoutMS,
|
|
7318
|
+
});
|
|
7163
7319
|
}
|
|
7164
7320
|
this.reportError = options.errorReporter || _.bind(logger$4.error, logger$4);
|
|
7165
7321
|
|
|
@@ -7167,6 +7323,14 @@ var RequestQueue = function (storageKey, options) {
|
|
|
7167
7323
|
|
|
7168
7324
|
this.memQueue = [];
|
|
7169
7325
|
this.initialized = false;
|
|
7326
|
+
|
|
7327
|
+
if (options.enqueueThrottleMs) {
|
|
7328
|
+
this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
|
|
7329
|
+
} else {
|
|
7330
|
+
this.enqueuePersisted = _.bind(function (queueEntry) {
|
|
7331
|
+
return this._enqueuePersisted([queueEntry]);
|
|
7332
|
+
}, this);
|
|
7333
|
+
}
|
|
7170
7334
|
};
|
|
7171
7335
|
|
|
7172
7336
|
RequestQueue.prototype.ensureInit = function () {
|
|
@@ -7209,36 +7373,39 @@ RequestQueue.prototype.enqueue = function (item, flushInterval) {
|
|
|
7209
7373
|
this.memQueue.push(queueEntry);
|
|
7210
7374
|
return PromisePolyfill.resolve(true);
|
|
7211
7375
|
} else {
|
|
7376
|
+
return this.enqueuePersisted(queueEntry);
|
|
7377
|
+
}
|
|
7378
|
+
};
|
|
7212
7379
|
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
return succeeded;
|
|
7228
|
-
}, this))
|
|
7229
|
-
.catch(_.bind(function (err) {
|
|
7230
|
-
this.reportError('Error enqueueing item', err, item);
|
|
7231
|
-
return false;
|
|
7232
|
-
}, this));
|
|
7233
|
-
}, this);
|
|
7380
|
+
RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
|
|
7381
|
+
var enqueueItem = _.bind(function () {
|
|
7382
|
+
return this.ensureInit()
|
|
7383
|
+
.then(_.bind(function () {
|
|
7384
|
+
return this.readFromStorage();
|
|
7385
|
+
}, this))
|
|
7386
|
+
.then(_.bind(function (storedQueue) {
|
|
7387
|
+
return this.saveToStorage(storedQueue.concat(queueEntries));
|
|
7388
|
+
}, this))
|
|
7389
|
+
.then(_.bind(function (succeeded) {
|
|
7390
|
+
// only add to in-memory queue when storage succeeds
|
|
7391
|
+
if (succeeded) {
|
|
7392
|
+
this.memQueue = this.memQueue.concat(queueEntries);
|
|
7393
|
+
}
|
|
7234
7394
|
|
|
7235
|
-
|
|
7236
|
-
|
|
7395
|
+
return succeeded;
|
|
7396
|
+
}, this))
|
|
7237
7397
|
.catch(_.bind(function (err) {
|
|
7238
|
-
this.reportError('Error
|
|
7398
|
+
this.reportError('Error enqueueing items', err, queueEntries);
|
|
7239
7399
|
return false;
|
|
7240
7400
|
}, this));
|
|
7241
|
-
}
|
|
7401
|
+
}, this);
|
|
7402
|
+
|
|
7403
|
+
return this.lock
|
|
7404
|
+
.withLock(enqueueItem, this.pid)
|
|
7405
|
+
.catch(_.bind(function (err) {
|
|
7406
|
+
this.reportError('Error acquiring storage lock', err);
|
|
7407
|
+
return false;
|
|
7408
|
+
}, this));
|
|
7242
7409
|
};
|
|
7243
7410
|
|
|
7244
7411
|
/**
|
|
@@ -7259,7 +7426,7 @@ RequestQueue.prototype.fillBatch = function (batchSize) {
|
|
|
7259
7426
|
}, this))
|
|
7260
7427
|
.then(_.bind(function (storedQueue) {
|
|
7261
7428
|
if (storedQueue.length) {
|
|
7262
|
-
|
|
7429
|
+
// item IDs already in batch; don't duplicate out of storage
|
|
7263
7430
|
var idsInBatch = {}; // poor man's Set
|
|
7264
7431
|
_.each(batch, function (item) {
|
|
7265
7432
|
idsInBatch[item['id']] = true;
|
|
@@ -7346,7 +7513,7 @@ RequestQueue.prototype.removeItemsByID = function (ids) {
|
|
|
7346
7513
|
.withLock(removeFromStorage, this.pid)
|
|
7347
7514
|
.catch(_.bind(function (err) {
|
|
7348
7515
|
this.reportError('Error acquiring storage lock', err);
|
|
7349
|
-
if (!localStorageSupported(this.
|
|
7516
|
+
if (!localStorageSupported(this.lock.storage, true)) {
|
|
7350
7517
|
// Looks like localStorage writes have stopped working sometime after
|
|
7351
7518
|
// initialization (probably full), and so nobody can acquire locks
|
|
7352
7519
|
// anymore. Consider it temporarily safe to remove items without the
|
|
@@ -7434,7 +7601,6 @@ RequestQueue.prototype.readFromStorage = function () {
|
|
|
7434
7601
|
}, this))
|
|
7435
7602
|
.then(_.bind(function (storageEntry) {
|
|
7436
7603
|
if (storageEntry) {
|
|
7437
|
-
storageEntry = JSONParse(storageEntry);
|
|
7438
7604
|
if (!_.isArray(storageEntry)) {
|
|
7439
7605
|
this.reportError('Invalid storage entry:', storageEntry);
|
|
7440
7606
|
storageEntry = null;
|
|
@@ -7452,16 +7618,9 @@ RequestQueue.prototype.readFromStorage = function () {
|
|
|
7452
7618
|
* Serialize the given items array to localStorage.
|
|
7453
7619
|
*/
|
|
7454
7620
|
RequestQueue.prototype.saveToStorage = function (queue) {
|
|
7455
|
-
try {
|
|
7456
|
-
var serialized = JSONStringify(queue);
|
|
7457
|
-
} catch (err) {
|
|
7458
|
-
this.reportError('Error serializing queue', err);
|
|
7459
|
-
return PromisePolyfill.resolve(false);
|
|
7460
|
-
}
|
|
7461
|
-
|
|
7462
7621
|
return this.ensureInit()
|
|
7463
7622
|
.then(_.bind(function () {
|
|
7464
|
-
return this.queueStorage.setItem(this.storageKey,
|
|
7623
|
+
return this.queueStorage.setItem(this.storageKey, queue);
|
|
7465
7624
|
}, this))
|
|
7466
7625
|
.then(function () {
|
|
7467
7626
|
return true;
|
|
@@ -7505,7 +7664,9 @@ var RequestBatcher = function(storageKey, options) {
|
|
|
7505
7664
|
errorReporter: _.bind(this.reportError, this),
|
|
7506
7665
|
queueStorage: options.queueStorage,
|
|
7507
7666
|
sharedLockStorage: options.sharedLockStorage,
|
|
7508
|
-
|
|
7667
|
+
sharedLockTimeoutMS: options.sharedLockTimeoutMS,
|
|
7668
|
+
usePersistence: options.usePersistence,
|
|
7669
|
+
enqueueThrottleMs: options.enqueueThrottleMs
|
|
7509
7670
|
});
|
|
7510
7671
|
|
|
7511
7672
|
this.libConfig = options.libConfig;
|
|
@@ -7527,6 +7688,8 @@ var RequestBatcher = function(storageKey, options) {
|
|
|
7527
7688
|
// as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
|
|
7528
7689
|
// in a request loop and get ratelimited by the server.
|
|
7529
7690
|
this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
|
|
7691
|
+
|
|
7692
|
+
this._flushPromise = null;
|
|
7530
7693
|
};
|
|
7531
7694
|
|
|
7532
7695
|
/**
|
|
@@ -7586,7 +7749,7 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
|
|
|
7586
7749
|
if (!this.stopped) { // don't schedule anymore if batching has been stopped
|
|
7587
7750
|
this.timeoutID = setTimeout(_.bind(function() {
|
|
7588
7751
|
if (!this.stopped) {
|
|
7589
|
-
this.flush();
|
|
7752
|
+
this._flushPromise = this.flush();
|
|
7590
7753
|
}
|
|
7591
7754
|
}, this), this.flushInterval);
|
|
7592
7755
|
}
|
|
@@ -7818,6 +7981,17 @@ RequestBatcher.prototype.reportError = function(msg, err) {
|
|
|
7818
7981
|
}
|
|
7819
7982
|
};
|
|
7820
7983
|
|
|
7984
|
+
/**
|
|
7985
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
7986
|
+
* @returns {boolean}
|
|
7987
|
+
*/
|
|
7988
|
+
var isRecordingExpired = function(serializedRecording) {
|
|
7989
|
+
var now = Date.now();
|
|
7990
|
+
return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
|
|
7991
|
+
};
|
|
7992
|
+
|
|
7993
|
+
var RECORD_ENQUEUE_THROTTLE_MS = 250;
|
|
7994
|
+
|
|
7821
7995
|
var logger$2 = console_with_prefix('recorder');
|
|
7822
7996
|
var CompressionStream = win['CompressionStream'];
|
|
7823
7997
|
|
|
@@ -7844,29 +8018,58 @@ function isUserEvent(ev) {
|
|
|
7844
8018
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7845
8019
|
}
|
|
7846
8020
|
|
|
8021
|
+
/**
|
|
8022
|
+
* @typedef {Object} SerializedRecording
|
|
8023
|
+
* @property {number} idleExpires
|
|
8024
|
+
* @property {number} maxExpires
|
|
8025
|
+
* @property {number} replayStartTime
|
|
8026
|
+
* @property {number} seqNo
|
|
8027
|
+
* @property {string} batchStartUrl
|
|
8028
|
+
* @property {string} replayId
|
|
8029
|
+
* @property {string} tabId
|
|
8030
|
+
* @property {string} replayStartUrl
|
|
8031
|
+
*/
|
|
8032
|
+
|
|
8033
|
+
/**
|
|
8034
|
+
* @typedef {Object} SessionRecordingOptions
|
|
8035
|
+
* @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
8036
|
+
* @property {String} [options.replayId] - unique uuid for a single replay
|
|
8037
|
+
* @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
8038
|
+
* @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
8039
|
+
* @property {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
8040
|
+
* @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
|
|
8041
|
+
* @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
|
|
8042
|
+
* optional properties for deserialization:
|
|
8043
|
+
* @property {number} idleExpires
|
|
8044
|
+
* @property {number} maxExpires
|
|
8045
|
+
* @property {number} replayStartTime
|
|
8046
|
+
* @property {number} seqNo
|
|
8047
|
+
* @property {string} batchStartUrl
|
|
8048
|
+
* @property {string} replayStartUrl
|
|
8049
|
+
*/
|
|
8050
|
+
|
|
8051
|
+
|
|
7847
8052
|
/**
|
|
7848
8053
|
* This class encapsulates a single session recording and its lifecycle.
|
|
7849
|
-
* @param {
|
|
7850
|
-
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7851
|
-
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7852
|
-
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7853
|
-
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
8054
|
+
* @param {SessionRecordingOptions} options
|
|
7854
8055
|
*/
|
|
7855
8056
|
var SessionRecording = function(options) {
|
|
7856
8057
|
this._mixpanel = options.mixpanelInstance;
|
|
7857
|
-
this._onIdleTimeout = options.onIdleTimeout;
|
|
7858
|
-
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7859
|
-
this.
|
|
7860
|
-
|
|
7861
|
-
this.replayId = options.replayId;
|
|
8058
|
+
this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
|
|
8059
|
+
this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
|
|
8060
|
+
this._onBatchSent = options.onBatchSent || NOOP_FUNC;
|
|
8061
|
+
this._rrwebRecord = options.rrwebRecord || null;
|
|
7862
8062
|
|
|
7863
8063
|
// internal rrweb stopRecording function
|
|
7864
8064
|
this._stopRecording = null;
|
|
8065
|
+
this.replayId = options.replayId;
|
|
7865
8066
|
|
|
7866
|
-
this.
|
|
7867
|
-
this.
|
|
7868
|
-
this.
|
|
7869
|
-
this.
|
|
8067
|
+
this.batchStartUrl = options.batchStartUrl || null;
|
|
8068
|
+
this.replayStartUrl = options.replayStartUrl || null;
|
|
8069
|
+
this.idleExpires = options.idleExpires || null;
|
|
8070
|
+
this.maxExpires = options.maxExpires || null;
|
|
8071
|
+
this.replayStartTime = options.replayStartTime || null;
|
|
8072
|
+
this.seqNo = options.seqNo || 0;
|
|
7870
8073
|
|
|
7871
8074
|
this.idleTimeoutId = null;
|
|
7872
8075
|
this.maxTimeoutId = null;
|
|
@@ -7874,18 +8077,40 @@ var SessionRecording = function(options) {
|
|
|
7874
8077
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7875
8078
|
this.recordMinMs = 0;
|
|
7876
8079
|
|
|
8080
|
+
// disable persistence if localStorage is not supported
|
|
8081
|
+
// request-queue will automatically disable persistence if indexedDB fails to initialize
|
|
8082
|
+
var usePersistence = localStorageSupported(options.sharedLockStorage, true);
|
|
8083
|
+
|
|
7877
8084
|
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7878
8085
|
// this will be important when persistence is introduced
|
|
7879
|
-
|
|
7880
|
-
this.
|
|
7881
|
-
|
|
8086
|
+
this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
|
|
8087
|
+
this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
|
|
8088
|
+
this.batcher = new RequestBatcher(this.batcherKey, {
|
|
8089
|
+
errorReporter: this.reportError.bind(this),
|
|
7882
8090
|
flushOnlyOnInterval: true,
|
|
7883
8091
|
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7884
|
-
sendRequestFunc:
|
|
7885
|
-
|
|
8092
|
+
sendRequestFunc: this.flushEventsWithOptOut.bind(this),
|
|
8093
|
+
queueStorage: this.queueStorage,
|
|
8094
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
8095
|
+
usePersistence: usePersistence,
|
|
8096
|
+
stopAllBatchingFunc: this.stopRecording.bind(this),
|
|
8097
|
+
|
|
8098
|
+
// increased throttle and shared lock timeout because recording events are very high frequency.
|
|
8099
|
+
// this will minimize the amount of lock contention between enqueued events.
|
|
8100
|
+
// for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
|
|
8101
|
+
enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
|
|
8102
|
+
sharedLockTimeoutMS: 10 * 1000,
|
|
7886
8103
|
});
|
|
7887
8104
|
};
|
|
7888
8105
|
|
|
8106
|
+
SessionRecording.prototype.unloadPersistedData = function () {
|
|
8107
|
+
this.batcher.stop();
|
|
8108
|
+
return this.batcher.flush()
|
|
8109
|
+
.then(function () {
|
|
8110
|
+
return this.queueStorage.removeItem(this.batcherKey);
|
|
8111
|
+
}.bind(this));
|
|
8112
|
+
};
|
|
8113
|
+
|
|
7889
8114
|
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7890
8115
|
return this._mixpanel.get_config(configVar);
|
|
7891
8116
|
};
|
|
@@ -7898,6 +8123,11 @@ SessionRecording.prototype.get_config = function(configVar) {
|
|
|
7898
8123
|
};
|
|
7899
8124
|
|
|
7900
8125
|
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
8126
|
+
if (this._rrwebRecord === null) {
|
|
8127
|
+
this.reportError('rrweb record function not provided. ');
|
|
8128
|
+
return;
|
|
8129
|
+
}
|
|
8130
|
+
|
|
7901
8131
|
if (this._stopRecording !== null) {
|
|
7902
8132
|
logger$2.log('Recording already in progress, skipping startRecording.');
|
|
7903
8133
|
return;
|
|
@@ -7909,15 +8139,21 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7909
8139
|
logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7910
8140
|
}
|
|
7911
8141
|
|
|
8142
|
+
if (!this.maxExpires) {
|
|
8143
|
+
this.maxExpires = new Date().getTime() + this.recordMaxMs;
|
|
8144
|
+
}
|
|
8145
|
+
|
|
7912
8146
|
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7913
8147
|
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7914
8148
|
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7915
8149
|
logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7916
8150
|
}
|
|
7917
8151
|
|
|
7918
|
-
this.replayStartTime
|
|
7919
|
-
|
|
7920
|
-
|
|
8152
|
+
if (!this.replayStartTime) {
|
|
8153
|
+
this.replayStartTime = new Date().getTime();
|
|
8154
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
8155
|
+
this.replayStartUrl = _.info.currentUrl();
|
|
8156
|
+
}
|
|
7921
8157
|
|
|
7922
8158
|
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7923
8159
|
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
@@ -7930,10 +8166,12 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7930
8166
|
this.batcher.start();
|
|
7931
8167
|
}
|
|
7932
8168
|
|
|
7933
|
-
var resetIdleTimeout =
|
|
8169
|
+
var resetIdleTimeout = function () {
|
|
7934
8170
|
clearTimeout(this.idleTimeoutId);
|
|
7935
|
-
|
|
7936
|
-
|
|
8171
|
+
var idleTimeoutMs = this.getConfig('record_idle_timeout_ms');
|
|
8172
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, idleTimeoutMs);
|
|
8173
|
+
this.idleExpires = new Date().getTime() + idleTimeoutMs;
|
|
8174
|
+
}.bind(this);
|
|
7937
8175
|
|
|
7938
8176
|
var blockSelector = this.getConfig('record_block_selector');
|
|
7939
8177
|
if (blockSelector === '' || blockSelector === null) {
|
|
@@ -7941,8 +8179,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7941
8179
|
}
|
|
7942
8180
|
|
|
7943
8181
|
this._stopRecording = this._rrwebRecord({
|
|
7944
|
-
'emit':
|
|
7945
|
-
this.batcher.enqueue(ev);
|
|
8182
|
+
'emit': addOptOutCheckMixpanelLib(function (ev) {
|
|
7946
8183
|
if (isUserEvent(ev)) {
|
|
7947
8184
|
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
7948
8185
|
// start flushing again after user activity
|
|
@@ -7950,7 +8187,10 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7950
8187
|
}
|
|
7951
8188
|
resetIdleTimeout();
|
|
7952
8189
|
}
|
|
7953
|
-
|
|
8190
|
+
|
|
8191
|
+
// promise only used to await during tests
|
|
8192
|
+
this.__enqueuePromise = this.batcher.enqueue(ev);
|
|
8193
|
+
}.bind(this)),
|
|
7954
8194
|
'blockClass': this.getConfig('record_block_class'),
|
|
7955
8195
|
'blockSelector': blockSelector,
|
|
7956
8196
|
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
@@ -7976,10 +8216,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7976
8216
|
|
|
7977
8217
|
resetIdleTimeout();
|
|
7978
8218
|
|
|
7979
|
-
|
|
8219
|
+
var maxTimeoutMs = this.maxExpires - new Date().getTime();
|
|
8220
|
+
this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
|
|
7980
8221
|
};
|
|
7981
8222
|
|
|
7982
|
-
SessionRecording.prototype.stopRecording = function () {
|
|
8223
|
+
SessionRecording.prototype.stopRecording = function (skipFlush) {
|
|
7983
8224
|
if (!this.isRrwebStopped()) {
|
|
7984
8225
|
try {
|
|
7985
8226
|
this._stopRecording();
|
|
@@ -7989,17 +8230,19 @@ SessionRecording.prototype.stopRecording = function () {
|
|
|
7989
8230
|
this._stopRecording = null;
|
|
7990
8231
|
}
|
|
7991
8232
|
|
|
8233
|
+
var flushPromise;
|
|
7992
8234
|
if (this.batcher.stopped) {
|
|
7993
8235
|
// never got user activity to flush after reset, so just clear the batcher
|
|
7994
|
-
this.batcher.clear();
|
|
7995
|
-
} else {
|
|
8236
|
+
flushPromise = this.batcher.clear();
|
|
8237
|
+
} else if (!skipFlush) {
|
|
7996
8238
|
// flush any remaining events from running batcher
|
|
7997
|
-
this.batcher.flush();
|
|
7998
|
-
this.batcher.stop();
|
|
8239
|
+
flushPromise = this.batcher.flush();
|
|
7999
8240
|
}
|
|
8241
|
+
this.batcher.stop();
|
|
8000
8242
|
|
|
8001
8243
|
clearTimeout(this.idleTimeoutId);
|
|
8002
8244
|
clearTimeout(this.maxTimeoutId);
|
|
8245
|
+
return flushPromise;
|
|
8003
8246
|
};
|
|
8004
8247
|
|
|
8005
8248
|
SessionRecording.prototype.isRrwebStopped = function () {
|
|
@@ -8011,7 +8254,54 @@ SessionRecording.prototype.isRrwebStopped = function () {
|
|
|
8011
8254
|
* we stop recording and dump any queued events if the user has opted out.
|
|
8012
8255
|
*/
|
|
8013
8256
|
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
8014
|
-
this._flushEvents(data, options, cb,
|
|
8257
|
+
this._flushEvents(data, options, cb, this._onOptOut.bind(this));
|
|
8258
|
+
};
|
|
8259
|
+
|
|
8260
|
+
/**
|
|
8261
|
+
* @returns {SerializedRecording}
|
|
8262
|
+
*/
|
|
8263
|
+
SessionRecording.prototype.serialize = function () {
|
|
8264
|
+
// don't break if mixpanel instance was destroyed at some point
|
|
8265
|
+
var tabId;
|
|
8266
|
+
try {
|
|
8267
|
+
tabId = this._mixpanel.get_tab_id();
|
|
8268
|
+
} catch (e) {
|
|
8269
|
+
this.reportError('Error getting tab ID for serialization ', e);
|
|
8270
|
+
tabId = null;
|
|
8271
|
+
}
|
|
8272
|
+
|
|
8273
|
+
return {
|
|
8274
|
+
'replayId': this.replayId,
|
|
8275
|
+
'seqNo': this.seqNo,
|
|
8276
|
+
'replayStartTime': this.replayStartTime,
|
|
8277
|
+
'batchStartUrl': this.batchStartUrl,
|
|
8278
|
+
'replayStartUrl': this.replayStartUrl,
|
|
8279
|
+
'idleExpires': this.idleExpires,
|
|
8280
|
+
'maxExpires': this.maxExpires,
|
|
8281
|
+
'tabId': tabId,
|
|
8282
|
+
};
|
|
8283
|
+
};
|
|
8284
|
+
|
|
8285
|
+
|
|
8286
|
+
/**
|
|
8287
|
+
* @static
|
|
8288
|
+
* @param {SerializedRecording} serializedRecording
|
|
8289
|
+
* @param {SessionRecordingOptions} options
|
|
8290
|
+
* @returns {SessionRecording}
|
|
8291
|
+
*/
|
|
8292
|
+
SessionRecording.deserialize = function (serializedRecording, options) {
|
|
8293
|
+
var recording = new SessionRecording(_.extend({}, options, {
|
|
8294
|
+
replayId: serializedRecording['replayId'],
|
|
8295
|
+
batchStartUrl: serializedRecording['batchStartUrl'],
|
|
8296
|
+
replayStartUrl: serializedRecording['replayStartUrl'],
|
|
8297
|
+
idleExpires: serializedRecording['idleExpires'],
|
|
8298
|
+
maxExpires: serializedRecording['maxExpires'],
|
|
8299
|
+
replayStartTime: serializedRecording['replayStartTime'],
|
|
8300
|
+
seqNo: serializedRecording['seqNo'],
|
|
8301
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
8302
|
+
}));
|
|
8303
|
+
|
|
8304
|
+
return recording;
|
|
8015
8305
|
};
|
|
8016
8306
|
|
|
8017
8307
|
SessionRecording.prototype._onOptOut = function (code) {
|
|
@@ -8022,7 +8312,7 @@ SessionRecording.prototype._onOptOut = function (code) {
|
|
|
8022
8312
|
};
|
|
8023
8313
|
|
|
8024
8314
|
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
8025
|
-
var onSuccess =
|
|
8315
|
+
var onSuccess = function (response, responseBody) {
|
|
8026
8316
|
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
8027
8317
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
8028
8318
|
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
@@ -8030,13 +8320,15 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
8030
8320
|
this.seqNo++;
|
|
8031
8321
|
this.batchStartUrl = _.info.currentUrl();
|
|
8032
8322
|
}
|
|
8323
|
+
|
|
8324
|
+
this._onBatchSent();
|
|
8033
8325
|
callback({
|
|
8034
8326
|
status: 0,
|
|
8035
8327
|
httpStatusCode: response.status,
|
|
8036
8328
|
responseBody: responseBody,
|
|
8037
8329
|
retryAfter: response.headers.get('Retry-After')
|
|
8038
8330
|
});
|
|
8039
|
-
}
|
|
8331
|
+
}.bind(this);
|
|
8040
8332
|
|
|
8041
8333
|
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
8042
8334
|
'method': 'POST',
|
|
@@ -8057,7 +8349,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
8057
8349
|
};
|
|
8058
8350
|
|
|
8059
8351
|
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
8060
|
-
|
|
8352
|
+
var numEvents = data.length;
|
|
8061
8353
|
|
|
8062
8354
|
if (numEvents > 0) {
|
|
8063
8355
|
var replayId = this.replayId;
|
|
@@ -8102,10 +8394,10 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
|
|
|
8102
8394
|
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
8103
8395
|
new Response(gzipStream)
|
|
8104
8396
|
.blob()
|
|
8105
|
-
.then(
|
|
8397
|
+
.then(function(compressedBlob) {
|
|
8106
8398
|
reqParams['format'] = 'gzip';
|
|
8107
8399
|
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
8108
|
-
}
|
|
8400
|
+
}.bind(this));
|
|
8109
8401
|
} else {
|
|
8110
8402
|
reqParams['format'] = 'body';
|
|
8111
8403
|
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
@@ -8126,54 +8418,208 @@ SessionRecording.prototype.reportError = function(msg, err) {
|
|
|
8126
8418
|
}
|
|
8127
8419
|
};
|
|
8128
8420
|
|
|
8421
|
+
/**
|
|
8422
|
+
* Module for handling the storage and retrieval of recording metadata as well as any active recordings.
|
|
8423
|
+
* Makes sure that only one tab can be recording at a time.
|
|
8424
|
+
*/
|
|
8425
|
+
var RecordingRegistry = function (options) {
|
|
8426
|
+
this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
8427
|
+
this.errorReporter = options.errorReporter;
|
|
8428
|
+
this.mixpanelInstance = options.mixpanelInstance;
|
|
8429
|
+
this.sharedLockStorage = options.sharedLockStorage;
|
|
8430
|
+
};
|
|
8431
|
+
|
|
8432
|
+
RecordingRegistry.prototype.handleError = function (err) {
|
|
8433
|
+
this.errorReporter('IndexedDB error: ', err);
|
|
8434
|
+
};
|
|
8435
|
+
|
|
8436
|
+
/**
|
|
8437
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
8438
|
+
*/
|
|
8439
|
+
RecordingRegistry.prototype.setActiveRecording = function (serializedRecording) {
|
|
8440
|
+
var tabId = serializedRecording['tabId'];
|
|
8441
|
+
if (!tabId) {
|
|
8442
|
+
console.warn('No tab ID is set, cannot persist recording metadata.');
|
|
8443
|
+
return PromisePolyfill.resolve();
|
|
8444
|
+
}
|
|
8445
|
+
|
|
8446
|
+
return this.idb.init()
|
|
8447
|
+
.then(function () {
|
|
8448
|
+
return this.idb.setItem(tabId, serializedRecording);
|
|
8449
|
+
}.bind(this))
|
|
8450
|
+
.catch(this.handleError.bind(this));
|
|
8451
|
+
};
|
|
8452
|
+
|
|
8453
|
+
/**
|
|
8454
|
+
* @returns {Promise<import('./session-recording').SerializedRecording>}
|
|
8455
|
+
*/
|
|
8456
|
+
RecordingRegistry.prototype.getActiveRecording = function () {
|
|
8457
|
+
return this.idb.init()
|
|
8458
|
+
.then(function () {
|
|
8459
|
+
return this.idb.getItem(this.mixpanelInstance.get_tab_id());
|
|
8460
|
+
}.bind(this))
|
|
8461
|
+
.then(function (serializedRecording) {
|
|
8462
|
+
return isRecordingExpired(serializedRecording) ? null : serializedRecording;
|
|
8463
|
+
}.bind(this))
|
|
8464
|
+
.catch(this.handleError.bind(this));
|
|
8465
|
+
};
|
|
8466
|
+
|
|
8467
|
+
RecordingRegistry.prototype.clearActiveRecording = function () {
|
|
8468
|
+
// mark recording as expired instead of deleting it in case the page unloads mid-flush and doesn't make it to ingestion.
|
|
8469
|
+
// this will ensure the next pageload will flush the remaining events, but not try to continue the recording.
|
|
8470
|
+
return this.getActiveRecording()
|
|
8471
|
+
.then(function (serializedRecording) {
|
|
8472
|
+
if (serializedRecording) {
|
|
8473
|
+
serializedRecording['maxExpires'] = 0;
|
|
8474
|
+
return this.setActiveRecording(serializedRecording);
|
|
8475
|
+
}
|
|
8476
|
+
}.bind(this))
|
|
8477
|
+
.catch(this.handleError.bind(this));
|
|
8478
|
+
};
|
|
8479
|
+
|
|
8480
|
+
/**
|
|
8481
|
+
* Flush any inactive recordings from the registry to minimize data loss.
|
|
8482
|
+
* The main idea here is that we can flush remaining rrweb events on the next page load if a tab is closed mid-batch.
|
|
8483
|
+
*/
|
|
8484
|
+
RecordingRegistry.prototype.flushInactiveRecordings = function () {
|
|
8485
|
+
return this.idb.init()
|
|
8486
|
+
.then(function() {
|
|
8487
|
+
return this.idb.getAll();
|
|
8488
|
+
}.bind(this))
|
|
8489
|
+
.then(function (serializedRecordings) {
|
|
8490
|
+
// clean up any expired recordings from the registry, non-expired ones may be active in other tabs
|
|
8491
|
+
var unloadPromises = serializedRecordings
|
|
8492
|
+
.filter(function (serializedRecording) {
|
|
8493
|
+
return isRecordingExpired(serializedRecording);
|
|
8494
|
+
})
|
|
8495
|
+
.map(function (serializedRecording) {
|
|
8496
|
+
var sessionRecording = SessionRecording.deserialize(serializedRecording, {
|
|
8497
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8498
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8499
|
+
});
|
|
8500
|
+
return sessionRecording.unloadPersistedData()
|
|
8501
|
+
.then(function () {
|
|
8502
|
+
// expired recording was successfully flushed, we can clean it up from the registry
|
|
8503
|
+
return this.idb.removeItem(serializedRecording['tabId']);
|
|
8504
|
+
}.bind(this))
|
|
8505
|
+
.catch(this.handleError.bind(this));
|
|
8506
|
+
}.bind(this));
|
|
8507
|
+
|
|
8508
|
+
return PromisePolyfill.all(unloadPromises);
|
|
8509
|
+
}.bind(this))
|
|
8510
|
+
.catch(this.handleError.bind(this));
|
|
8511
|
+
};
|
|
8512
|
+
|
|
8129
8513
|
var logger$1 = console_with_prefix('recorder');
|
|
8130
8514
|
|
|
8131
8515
|
/**
|
|
8132
|
-
* Recorder API:
|
|
8516
|
+
* Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
|
|
8133
8517
|
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
8134
|
-
|
|
8135
|
-
var MixpanelRecorder = function(mixpanelInstance) {
|
|
8136
|
-
this.
|
|
8518
|
+
*/
|
|
8519
|
+
var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage) {
|
|
8520
|
+
this.mixpanelInstance = mixpanelInstance;
|
|
8521
|
+
this.rrwebRecord = rrwebRecord || record;
|
|
8522
|
+
this.sharedLockStorage = sharedLockStorage;
|
|
8523
|
+
|
|
8524
|
+
/**
|
|
8525
|
+
* @member {import('./registry').RecordingRegistry}
|
|
8526
|
+
*/
|
|
8527
|
+
this.recordingRegistry = new RecordingRegistry({
|
|
8528
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8529
|
+
errorReporter: logger$1.error,
|
|
8530
|
+
sharedLockStorage: sharedLockStorage
|
|
8531
|
+
});
|
|
8532
|
+
this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
|
|
8533
|
+
|
|
8137
8534
|
this.activeRecording = null;
|
|
8138
8535
|
};
|
|
8139
8536
|
|
|
8140
|
-
MixpanelRecorder.prototype.startRecording = function(
|
|
8537
|
+
MixpanelRecorder.prototype.startRecording = function(options) {
|
|
8538
|
+
options = options || {};
|
|
8141
8539
|
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
8142
8540
|
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
8143
8541
|
return;
|
|
8144
8542
|
}
|
|
8145
8543
|
|
|
8146
|
-
var onIdleTimeout =
|
|
8544
|
+
var onIdleTimeout = function () {
|
|
8147
8545
|
logger$1.log('Idle timeout reached, restarting recording.');
|
|
8148
8546
|
this.resetRecording();
|
|
8149
|
-
}
|
|
8547
|
+
}.bind(this);
|
|
8150
8548
|
|
|
8151
|
-
var onMaxLengthReached =
|
|
8549
|
+
var onMaxLengthReached = function () {
|
|
8152
8550
|
logger$1.log('Max recording length reached, stopping recording.');
|
|
8153
8551
|
this.resetRecording();
|
|
8154
|
-
}
|
|
8552
|
+
}.bind(this);
|
|
8553
|
+
|
|
8554
|
+
var onBatchSent = function () {
|
|
8555
|
+
this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8556
|
+
this['__flushPromise'] = this.activeRecording.batcher._flushPromise;
|
|
8557
|
+
}.bind(this);
|
|
8155
8558
|
|
|
8156
|
-
|
|
8157
|
-
|
|
8559
|
+
/**
|
|
8560
|
+
* @type {import('./session-recording').SessionRecordingOptions}
|
|
8561
|
+
*/
|
|
8562
|
+
var sessionRecordingOptions = {
|
|
8563
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8564
|
+
onBatchSent: onBatchSent,
|
|
8158
8565
|
onIdleTimeout: onIdleTimeout,
|
|
8159
8566
|
onMaxLengthReached: onMaxLengthReached,
|
|
8160
8567
|
replayId: _.UUID(),
|
|
8161
|
-
rrwebRecord:
|
|
8162
|
-
|
|
8568
|
+
rrwebRecord: this.rrwebRecord,
|
|
8569
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8570
|
+
};
|
|
8571
|
+
|
|
8572
|
+
if (options.activeSerializedRecording) {
|
|
8573
|
+
this.activeRecording = SessionRecording.deserialize(options.activeSerializedRecording, sessionRecordingOptions);
|
|
8574
|
+
} else {
|
|
8575
|
+
this.activeRecording = new SessionRecording(sessionRecordingOptions);
|
|
8576
|
+
}
|
|
8163
8577
|
|
|
8164
|
-
this.activeRecording.startRecording(shouldStopBatcher);
|
|
8578
|
+
this.activeRecording.startRecording(options.shouldStopBatcher);
|
|
8579
|
+
return this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8165
8580
|
};
|
|
8166
8581
|
|
|
8167
8582
|
MixpanelRecorder.prototype.stopRecording = function() {
|
|
8583
|
+
var stopPromise = this._stopCurrentRecording(false);
|
|
8584
|
+
this.recordingRegistry.clearActiveRecording();
|
|
8585
|
+
this.activeRecording = null;
|
|
8586
|
+
return stopPromise;
|
|
8587
|
+
};
|
|
8588
|
+
|
|
8589
|
+
MixpanelRecorder.prototype.pauseRecording = function() {
|
|
8590
|
+
return this._stopCurrentRecording(false);
|
|
8591
|
+
};
|
|
8592
|
+
|
|
8593
|
+
MixpanelRecorder.prototype._stopCurrentRecording = function(skipFlush) {
|
|
8168
8594
|
if (this.activeRecording) {
|
|
8169
|
-
this.activeRecording.stopRecording();
|
|
8170
|
-
this.activeRecording = null;
|
|
8595
|
+
return this.activeRecording.stopRecording(skipFlush);
|
|
8171
8596
|
}
|
|
8597
|
+
return PromisePolyfill.resolve();
|
|
8598
|
+
};
|
|
8599
|
+
|
|
8600
|
+
MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
|
|
8601
|
+
if (this.activeRecording && this.activeRecording.isRrwebStopped()) {
|
|
8602
|
+
this.activeRecording.startRecording(false);
|
|
8603
|
+
return PromisePolyfill.resolve(null);
|
|
8604
|
+
}
|
|
8605
|
+
|
|
8606
|
+
return this.recordingRegistry.getActiveRecording()
|
|
8607
|
+
.then(function (activeSerializedRecording) {
|
|
8608
|
+
if (activeSerializedRecording) {
|
|
8609
|
+
return this.startRecording({activeSerializedRecording: activeSerializedRecording});
|
|
8610
|
+
} else if (startNewIfInactive) {
|
|
8611
|
+
return this.startRecording({shouldStopBatcher: false});
|
|
8612
|
+
} else {
|
|
8613
|
+
logger$1.log('No resumable recording found.');
|
|
8614
|
+
return null;
|
|
8615
|
+
}
|
|
8616
|
+
}.bind(this));
|
|
8172
8617
|
};
|
|
8173
8618
|
|
|
8619
|
+
|
|
8174
8620
|
MixpanelRecorder.prototype.resetRecording = function () {
|
|
8175
8621
|
this.stopRecording();
|
|
8176
|
-
this.startRecording(true);
|
|
8622
|
+
this.startRecording({shouldStopBatcher: true});
|
|
8177
8623
|
};
|
|
8178
8624
|
|
|
8179
8625
|
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
@@ -10389,11 +10835,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
|
|
|
10389
10835
|
* Released under the MIT License.
|
|
10390
10836
|
*/
|
|
10391
10837
|
|
|
10392
|
-
// ==ClosureCompiler==
|
|
10393
|
-
// @compilation_level ADVANCED_OPTIMIZATIONS
|
|
10394
|
-
// @output_file_name mixpanel-2.8.min.js
|
|
10395
|
-
// ==/ClosureCompiler==
|
|
10396
|
-
|
|
10397
10838
|
/*
|
|
10398
10839
|
SIMPLE STYLE GUIDE:
|
|
10399
10840
|
|
|
@@ -10416,7 +10857,6 @@ var INIT_MODULE = 0;
|
|
|
10416
10857
|
var INIT_SNIPPET = 1;
|
|
10417
10858
|
|
|
10418
10859
|
var IDENTITY_FUNC = function(x) {return x;};
|
|
10419
|
-
var NOOP_FUNC = function() {};
|
|
10420
10860
|
|
|
10421
10861
|
/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
|
|
10422
10862
|
/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
|
|
@@ -10725,34 +11165,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
10725
11165
|
this.autocapture = new Autocapture(this);
|
|
10726
11166
|
this.autocapture.init();
|
|
10727
11167
|
|
|
10728
|
-
|
|
10729
|
-
|
|
11168
|
+
this._init_tab_id();
|
|
11169
|
+
this._check_and_start_session_recording();
|
|
11170
|
+
};
|
|
11171
|
+
|
|
11172
|
+
/**
|
|
11173
|
+
* Assigns a unique UUID to this tab / window by leveraging sessionStorage.
|
|
11174
|
+
* This is primarily used for session recording, where data must be isolated to the current tab.
|
|
11175
|
+
*/
|
|
11176
|
+
MixpanelLib.prototype._init_tab_id = function() {
|
|
11177
|
+
if (_.sessionStorage.is_supported()) {
|
|
11178
|
+
try {
|
|
11179
|
+
var key_suffix = this.get_config('name') + '_' + this.get_config('token');
|
|
11180
|
+
var tab_id_key = 'mp_tab_id_' + key_suffix;
|
|
11181
|
+
|
|
11182
|
+
// A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
|
|
11183
|
+
// This enforces a unique ID in the cases like duplicated tab, window.open(...)
|
|
11184
|
+
var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
|
|
11185
|
+
if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
|
|
11186
|
+
_.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
|
|
11187
|
+
}
|
|
11188
|
+
|
|
11189
|
+
_.sessionStorage.set(should_generate_new_tab_id_key, '1');
|
|
11190
|
+
this.tab_id = _.sessionStorage.get(tab_id_key);
|
|
11191
|
+
|
|
11192
|
+
// Remove the flag when the tab is unloaded to indicate the stored tab ID can be reused. This event is not reliable to detect all page unloads,
|
|
11193
|
+
// but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
|
|
11194
|
+
// If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
|
|
11195
|
+
win.addEventListener('beforeunload', function () {
|
|
11196
|
+
_.sessionStorage.remove(should_generate_new_tab_id_key);
|
|
11197
|
+
});
|
|
11198
|
+
} catch(err) {
|
|
11199
|
+
this.report_error('Error initializing tab id', err);
|
|
11200
|
+
}
|
|
11201
|
+
} else {
|
|
11202
|
+
this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
|
|
10730
11203
|
}
|
|
10731
11204
|
};
|
|
10732
11205
|
|
|
10733
|
-
MixpanelLib.prototype.
|
|
11206
|
+
MixpanelLib.prototype.get_tab_id = function () {
|
|
11207
|
+
return this.tab_id || null;
|
|
11208
|
+
};
|
|
11209
|
+
|
|
11210
|
+
MixpanelLib.prototype._should_load_recorder = function () {
|
|
11211
|
+
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
11212
|
+
var tab_id = this.get_tab_id();
|
|
11213
|
+
return recording_registry_idb.init()
|
|
11214
|
+
.then(function () {
|
|
11215
|
+
return recording_registry_idb.getAll();
|
|
11216
|
+
})
|
|
11217
|
+
.then(function (recordings) {
|
|
11218
|
+
for (var i = 0; i < recordings.length; i++) {
|
|
11219
|
+
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
11220
|
+
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
11221
|
+
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
11222
|
+
return true;
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
return false;
|
|
11226
|
+
})
|
|
11227
|
+
.catch(_.bind(function (err) {
|
|
11228
|
+
this.report_error('Error checking recording registry', err);
|
|
11229
|
+
}, this));
|
|
11230
|
+
};
|
|
11231
|
+
|
|
11232
|
+
MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
|
|
10734
11233
|
if (!win['MutationObserver']) {
|
|
10735
11234
|
console$1.critical('Browser does not support MutationObserver; skipping session recording');
|
|
10736
11235
|
return;
|
|
10737
11236
|
}
|
|
10738
11237
|
|
|
10739
|
-
var
|
|
10740
|
-
|
|
10741
|
-
|
|
11238
|
+
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
11239
|
+
var handleLoadedRecorder = _.bind(function() {
|
|
11240
|
+
this._recorder = this._recorder || new win['__mp_recorder'](this);
|
|
11241
|
+
this._recorder['resumeRecording'](startNewIfInactive);
|
|
11242
|
+
}, this);
|
|
11243
|
+
|
|
11244
|
+
if (_.isUndefined(win['__mp_recorder'])) {
|
|
11245
|
+
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
11246
|
+
} else {
|
|
11247
|
+
handleLoadedRecorder();
|
|
11248
|
+
}
|
|
10742
11249
|
}, this);
|
|
10743
11250
|
|
|
10744
|
-
|
|
10745
|
-
|
|
11251
|
+
/**
|
|
11252
|
+
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
11253
|
+
* Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
|
|
11254
|
+
*/
|
|
11255
|
+
var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
|
|
11256
|
+
if (force_start || is_sampled) {
|
|
11257
|
+
loadRecorder(true);
|
|
10746
11258
|
} else {
|
|
10747
|
-
|
|
11259
|
+
this._should_load_recorder()
|
|
11260
|
+
.then(function (shouldLoad) {
|
|
11261
|
+
if (shouldLoad) {
|
|
11262
|
+
loadRecorder(false);
|
|
11263
|
+
}
|
|
11264
|
+
});
|
|
10748
11265
|
}
|
|
10749
11266
|
});
|
|
10750
11267
|
|
|
11268
|
+
MixpanelLib.prototype.start_session_recording = function () {
|
|
11269
|
+
this._check_and_start_session_recording(true);
|
|
11270
|
+
};
|
|
11271
|
+
|
|
10751
11272
|
MixpanelLib.prototype.stop_session_recording = function () {
|
|
10752
11273
|
if (this._recorder) {
|
|
10753
11274
|
this._recorder['stopRecording']();
|
|
10754
|
-
}
|
|
10755
|
-
|
|
11275
|
+
}
|
|
11276
|
+
};
|
|
11277
|
+
|
|
11278
|
+
MixpanelLib.prototype.pause_session_recording = function () {
|
|
11279
|
+
if (this._recorder) {
|
|
11280
|
+
this._recorder['pauseRecording']();
|
|
11281
|
+
}
|
|
11282
|
+
};
|
|
11283
|
+
|
|
11284
|
+
MixpanelLib.prototype.resume_session_recording = function () {
|
|
11285
|
+
if (this._recorder) {
|
|
11286
|
+
this._recorder['resumeRecording']();
|
|
10756
11287
|
}
|
|
10757
11288
|
};
|
|
10758
11289
|
|
|
@@ -10787,6 +11318,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
|
|
|
10787
11318
|
return replay_id || null;
|
|
10788
11319
|
};
|
|
10789
11320
|
|
|
11321
|
+
// "private" public method to reach into the recorder in test cases
|
|
11322
|
+
MixpanelLib.prototype.__get_recorder = function () {
|
|
11323
|
+
return this._recorder;
|
|
11324
|
+
};
|
|
11325
|
+
|
|
10790
11326
|
// Private methods
|
|
10791
11327
|
|
|
10792
11328
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -11126,7 +11662,8 @@ MixpanelLib.prototype.init_batchers = function() {
|
|
|
11126
11662
|
return this._run_hook('before_send_' + attrs.type, item);
|
|
11127
11663
|
}, this),
|
|
11128
11664
|
stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
|
|
11129
|
-
usePersistence: true
|
|
11665
|
+
usePersistence: true,
|
|
11666
|
+
enqueueThrottleMs: 10,
|
|
11130
11667
|
}
|
|
11131
11668
|
);
|
|
11132
11669
|
}, this);
|
|
@@ -12227,6 +12764,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
|
|
|
12227
12764
|
|
|
12228
12765
|
if (disabled) {
|
|
12229
12766
|
this.stop_batch_senders();
|
|
12767
|
+
this.stop_session_recording();
|
|
12230
12768
|
} else {
|
|
12231
12769
|
// only start batchers after opt-in if they have previously been started
|
|
12232
12770
|
// in order to avoid unintentionally starting up batching for the first time
|
|
@@ -12467,10 +13005,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
|
|
|
12467
13005
|
MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
|
|
12468
13006
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
12469
13007
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
13008
|
+
MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
|
|
13009
|
+
MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
|
|
12470
13010
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
12471
13011
|
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
13012
|
+
MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
|
|
12472
13013
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
12473
13014
|
|
|
13015
|
+
// Exports intended only for testing
|
|
13016
|
+
MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
|
|
13017
|
+
|
|
12474
13018
|
// MixpanelPersistence Exports
|
|
12475
13019
|
MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
|
|
12476
13020
|
MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;
|