mixpanel-browser 2.60.0 → 2.61.1
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 +8 -0
- package/README.md +2 -2
- package/dist/mixpanel-core.cjs.js +398 -128
- package/dist/mixpanel-recorder.js +721 -255
- 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 +837 -273
- package/dist/mixpanel.cjs.js +837 -273
- package/dist/mixpanel.globals.js +398 -128
- package/dist/mixpanel.min.js +143 -138
- package/dist/mixpanel.module.js +837 -273
- package/dist/mixpanel.umd.js +837 -273
- 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 +213 -74
- 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.1'
|
|
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
|
-
},
|
|
6009
|
-
|
|
6010
|
-
error: function(msg) {
|
|
6011
|
-
console$1.error('localStorage error: ' + msg);
|
|
6012
|
-
},
|
|
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
|
+
};
|
|
6013
5999
|
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
},
|
|
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
|
+
};
|
|
6022
6007
|
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
// noop
|
|
6028
|
-
}
|
|
6029
|
-
return null;
|
|
6030
|
-
},
|
|
6008
|
+
function _storageWrapper(storage, name, is_supported_fn) {
|
|
6009
|
+
var log_error = function(msg) {
|
|
6010
|
+
console$1.error(name + ' error: ' + msg);
|
|
6011
|
+
};
|
|
6031
6012
|
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
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
|
+
}
|
|
6037
6051
|
}
|
|
6038
|
-
}
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6039
6054
|
|
|
6040
|
-
|
|
6041
|
-
|
|
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 && 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,42 +8166,49 @@ 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) {
|
|
7940
8178
|
blockSelector = undefined;
|
|
7941
8179
|
}
|
|
7942
8180
|
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
8181
|
+
try {
|
|
8182
|
+
this._stopRecording = this._rrwebRecord({
|
|
8183
|
+
'emit': function (ev) {
|
|
8184
|
+
if (isUserEvent(ev)) {
|
|
8185
|
+
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
8186
|
+
// start flushing again after user activity
|
|
8187
|
+
this.batcher.start();
|
|
8188
|
+
}
|
|
8189
|
+
resetIdleTimeout();
|
|
7950
8190
|
}
|
|
7951
|
-
|
|
8191
|
+
// promise only used to await during tests
|
|
8192
|
+
this.__enqueuePromise = this.batcher.enqueue(ev);
|
|
8193
|
+
}.bind(this),
|
|
8194
|
+
'blockClass': this.getConfig('record_block_class'),
|
|
8195
|
+
'blockSelector': blockSelector,
|
|
8196
|
+
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
8197
|
+
'dataURLOptions': { // canvas image options (https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)
|
|
8198
|
+
'type': 'image/webp',
|
|
8199
|
+
'quality': 0.6
|
|
8200
|
+
},
|
|
8201
|
+
'maskAllInputs': true,
|
|
8202
|
+
'maskTextClass': this.getConfig('record_mask_text_class'),
|
|
8203
|
+
'maskTextSelector': this.getConfig('record_mask_text_selector'),
|
|
8204
|
+
'recordCanvas': this.getConfig('record_canvas'),
|
|
8205
|
+
'sampling': {
|
|
8206
|
+
'canvas': 15
|
|
7952
8207
|
}
|
|
7953
|
-
}
|
|
7954
|
-
|
|
7955
|
-
'
|
|
7956
|
-
|
|
7957
|
-
'dataURLOptions': { // canvas image options (https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)
|
|
7958
|
-
'type': 'image/webp',
|
|
7959
|
-
'quality': 0.6
|
|
7960
|
-
},
|
|
7961
|
-
'maskAllInputs': true,
|
|
7962
|
-
'maskTextClass': this.getConfig('record_mask_text_class'),
|
|
7963
|
-
'maskTextSelector': this.getConfig('record_mask_text_selector'),
|
|
7964
|
-
'recordCanvas': this.getConfig('record_canvas'),
|
|
7965
|
-
'sampling': {
|
|
7966
|
-
'canvas': 15
|
|
7967
|
-
}
|
|
7968
|
-
});
|
|
8208
|
+
});
|
|
8209
|
+
} catch (err) {
|
|
8210
|
+
this.reportError('Unexpected error when starting rrweb recording.', err);
|
|
8211
|
+
}
|
|
7969
8212
|
|
|
7970
8213
|
if (typeof this._stopRecording !== 'function') {
|
|
7971
8214
|
this.reportError('rrweb failed to start, skipping this recording.');
|
|
@@ -7976,10 +8219,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7976
8219
|
|
|
7977
8220
|
resetIdleTimeout();
|
|
7978
8221
|
|
|
7979
|
-
|
|
8222
|
+
var maxTimeoutMs = this.maxExpires - new Date().getTime();
|
|
8223
|
+
this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
|
|
7980
8224
|
};
|
|
7981
8225
|
|
|
7982
|
-
SessionRecording.prototype.stopRecording = function () {
|
|
8226
|
+
SessionRecording.prototype.stopRecording = function (skipFlush) {
|
|
7983
8227
|
if (!this.isRrwebStopped()) {
|
|
7984
8228
|
try {
|
|
7985
8229
|
this._stopRecording();
|
|
@@ -7989,40 +8233,91 @@ SessionRecording.prototype.stopRecording = function () {
|
|
|
7989
8233
|
this._stopRecording = null;
|
|
7990
8234
|
}
|
|
7991
8235
|
|
|
8236
|
+
var flushPromise;
|
|
7992
8237
|
if (this.batcher.stopped) {
|
|
7993
8238
|
// never got user activity to flush after reset, so just clear the batcher
|
|
7994
|
-
this.batcher.clear();
|
|
7995
|
-
} else {
|
|
8239
|
+
flushPromise = this.batcher.clear();
|
|
8240
|
+
} else if (!skipFlush) {
|
|
7996
8241
|
// flush any remaining events from running batcher
|
|
7997
|
-
this.batcher.flush();
|
|
7998
|
-
this.batcher.stop();
|
|
8242
|
+
flushPromise = this.batcher.flush();
|
|
7999
8243
|
}
|
|
8244
|
+
this.batcher.stop();
|
|
8000
8245
|
|
|
8001
8246
|
clearTimeout(this.idleTimeoutId);
|
|
8002
8247
|
clearTimeout(this.maxTimeoutId);
|
|
8248
|
+
return flushPromise;
|
|
8003
8249
|
};
|
|
8004
8250
|
|
|
8005
8251
|
SessionRecording.prototype.isRrwebStopped = function () {
|
|
8006
8252
|
return this._stopRecording === null;
|
|
8007
8253
|
};
|
|
8008
8254
|
|
|
8255
|
+
|
|
8009
8256
|
/**
|
|
8010
8257
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
8011
8258
|
* we stop recording and dump any queued events if the user has opted out.
|
|
8012
8259
|
*/
|
|
8013
8260
|
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
8014
|
-
|
|
8261
|
+
var onOptOut = function (code) {
|
|
8262
|
+
// addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
|
|
8263
|
+
if (code === 0) {
|
|
8264
|
+
this.stopRecording();
|
|
8265
|
+
cb({error: 'Tracking has been opted out, stopping recording.'});
|
|
8266
|
+
}
|
|
8267
|
+
}.bind(this);
|
|
8268
|
+
|
|
8269
|
+
this._flushEvents(data, options, cb, onOptOut);
|
|
8015
8270
|
};
|
|
8016
8271
|
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8272
|
+
/**
|
|
8273
|
+
* @returns {SerializedRecording}
|
|
8274
|
+
*/
|
|
8275
|
+
SessionRecording.prototype.serialize = function () {
|
|
8276
|
+
// don't break if mixpanel instance was destroyed at some point
|
|
8277
|
+
var tabId;
|
|
8278
|
+
try {
|
|
8279
|
+
tabId = this._mixpanel.get_tab_id();
|
|
8280
|
+
} catch (e) {
|
|
8281
|
+
this.reportError('Error getting tab ID for serialization ', e);
|
|
8282
|
+
tabId = null;
|
|
8021
8283
|
}
|
|
8284
|
+
|
|
8285
|
+
return {
|
|
8286
|
+
'replayId': this.replayId,
|
|
8287
|
+
'seqNo': this.seqNo,
|
|
8288
|
+
'replayStartTime': this.replayStartTime,
|
|
8289
|
+
'batchStartUrl': this.batchStartUrl,
|
|
8290
|
+
'replayStartUrl': this.replayStartUrl,
|
|
8291
|
+
'idleExpires': this.idleExpires,
|
|
8292
|
+
'maxExpires': this.maxExpires,
|
|
8293
|
+
'tabId': tabId,
|
|
8294
|
+
};
|
|
8295
|
+
};
|
|
8296
|
+
|
|
8297
|
+
|
|
8298
|
+
/**
|
|
8299
|
+
* @static
|
|
8300
|
+
* @param {SerializedRecording} serializedRecording
|
|
8301
|
+
* @param {SessionRecordingOptions} options
|
|
8302
|
+
* @returns {SessionRecording}
|
|
8303
|
+
*/
|
|
8304
|
+
SessionRecording.deserialize = function (serializedRecording, options) {
|
|
8305
|
+
var recording = new SessionRecording(_.extend({}, options, {
|
|
8306
|
+
replayId: serializedRecording['replayId'],
|
|
8307
|
+
batchStartUrl: serializedRecording['batchStartUrl'],
|
|
8308
|
+
replayStartUrl: serializedRecording['replayStartUrl'],
|
|
8309
|
+
idleExpires: serializedRecording['idleExpires'],
|
|
8310
|
+
maxExpires: serializedRecording['maxExpires'],
|
|
8311
|
+
replayStartTime: serializedRecording['replayStartTime'],
|
|
8312
|
+
seqNo: serializedRecording['seqNo'],
|
|
8313
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
8314
|
+
}));
|
|
8315
|
+
|
|
8316
|
+
return recording;
|
|
8022
8317
|
};
|
|
8023
8318
|
|
|
8024
8319
|
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
8025
|
-
var onSuccess =
|
|
8320
|
+
var onSuccess = function (response, responseBody) {
|
|
8026
8321
|
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
8027
8322
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
8028
8323
|
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
@@ -8030,13 +8325,15 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
8030
8325
|
this.seqNo++;
|
|
8031
8326
|
this.batchStartUrl = _.info.currentUrl();
|
|
8032
8327
|
}
|
|
8328
|
+
|
|
8329
|
+
this._onBatchSent();
|
|
8033
8330
|
callback({
|
|
8034
8331
|
status: 0,
|
|
8035
8332
|
httpStatusCode: response.status,
|
|
8036
8333
|
responseBody: responseBody,
|
|
8037
8334
|
retryAfter: response.headers.get('Retry-After')
|
|
8038
8335
|
});
|
|
8039
|
-
}
|
|
8336
|
+
}.bind(this);
|
|
8040
8337
|
|
|
8041
8338
|
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
8042
8339
|
'method': 'POST',
|
|
@@ -8057,21 +8354,36 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
8057
8354
|
};
|
|
8058
8355
|
|
|
8059
8356
|
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
8060
|
-
|
|
8357
|
+
var numEvents = data.length;
|
|
8061
8358
|
|
|
8062
8359
|
if (numEvents > 0) {
|
|
8063
8360
|
var replayId = this.replayId;
|
|
8361
|
+
|
|
8064
8362
|
// each rrweb event has a timestamp - leverage those to get time properties
|
|
8065
|
-
var batchStartTime =
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8363
|
+
var batchStartTime = Infinity;
|
|
8364
|
+
var batchEndTime = -Infinity;
|
|
8365
|
+
var hasFullSnapshot = false;
|
|
8366
|
+
for (var i = 0; i < numEvents; i++) {
|
|
8367
|
+
batchStartTime = Math.min(batchStartTime, data[i].timestamp);
|
|
8368
|
+
batchEndTime = Math.max(batchEndTime, data[i].timestamp);
|
|
8369
|
+
if (data[i].type === EventType.FullSnapshot) {
|
|
8370
|
+
hasFullSnapshot = true;
|
|
8070
8371
|
}
|
|
8372
|
+
}
|
|
8071
8373
|
|
|
8374
|
+
if (this.seqNo === 0) {
|
|
8375
|
+
if (!hasFullSnapshot) {
|
|
8376
|
+
callback({error: 'First batch does not contain a full snapshot. Aborting recording.'});
|
|
8377
|
+
this.stopRecording(true);
|
|
8378
|
+
return;
|
|
8379
|
+
}
|
|
8380
|
+
this.replayStartTime = batchStartTime;
|
|
8381
|
+
} else if (!this.replayStartTime) {
|
|
8382
|
+
this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
|
|
8072
8383
|
this.replayStartTime = batchStartTime;
|
|
8073
8384
|
}
|
|
8074
|
-
|
|
8385
|
+
|
|
8386
|
+
var replayLengthMs = batchEndTime - this.replayStartTime;
|
|
8075
8387
|
|
|
8076
8388
|
var reqParams = {
|
|
8077
8389
|
'$current_url': this.batchStartUrl,
|
|
@@ -8102,10 +8414,10 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
|
|
|
8102
8414
|
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
8103
8415
|
new Response(gzipStream)
|
|
8104
8416
|
.blob()
|
|
8105
|
-
.then(
|
|
8417
|
+
.then(function(compressedBlob) {
|
|
8106
8418
|
reqParams['format'] = 'gzip';
|
|
8107
8419
|
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
8108
|
-
}
|
|
8420
|
+
}.bind(this));
|
|
8109
8421
|
} else {
|
|
8110
8422
|
reqParams['format'] = 'body';
|
|
8111
8423
|
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
@@ -8126,54 +8438,208 @@ SessionRecording.prototype.reportError = function(msg, err) {
|
|
|
8126
8438
|
}
|
|
8127
8439
|
};
|
|
8128
8440
|
|
|
8441
|
+
/**
|
|
8442
|
+
* Module for handling the storage and retrieval of recording metadata as well as any active recordings.
|
|
8443
|
+
* Makes sure that only one tab can be recording at a time.
|
|
8444
|
+
*/
|
|
8445
|
+
var RecordingRegistry = function (options) {
|
|
8446
|
+
this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
8447
|
+
this.errorReporter = options.errorReporter;
|
|
8448
|
+
this.mixpanelInstance = options.mixpanelInstance;
|
|
8449
|
+
this.sharedLockStorage = options.sharedLockStorage;
|
|
8450
|
+
};
|
|
8451
|
+
|
|
8452
|
+
RecordingRegistry.prototype.handleError = function (err) {
|
|
8453
|
+
this.errorReporter('IndexedDB error: ', err);
|
|
8454
|
+
};
|
|
8455
|
+
|
|
8456
|
+
/**
|
|
8457
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
8458
|
+
*/
|
|
8459
|
+
RecordingRegistry.prototype.setActiveRecording = function (serializedRecording) {
|
|
8460
|
+
var tabId = serializedRecording['tabId'];
|
|
8461
|
+
if (!tabId) {
|
|
8462
|
+
console.warn('No tab ID is set, cannot persist recording metadata.');
|
|
8463
|
+
return PromisePolyfill.resolve();
|
|
8464
|
+
}
|
|
8465
|
+
|
|
8466
|
+
return this.idb.init()
|
|
8467
|
+
.then(function () {
|
|
8468
|
+
return this.idb.setItem(tabId, serializedRecording);
|
|
8469
|
+
}.bind(this))
|
|
8470
|
+
.catch(this.handleError.bind(this));
|
|
8471
|
+
};
|
|
8472
|
+
|
|
8473
|
+
/**
|
|
8474
|
+
* @returns {Promise<import('./session-recording').SerializedRecording>}
|
|
8475
|
+
*/
|
|
8476
|
+
RecordingRegistry.prototype.getActiveRecording = function () {
|
|
8477
|
+
return this.idb.init()
|
|
8478
|
+
.then(function () {
|
|
8479
|
+
return this.idb.getItem(this.mixpanelInstance.get_tab_id());
|
|
8480
|
+
}.bind(this))
|
|
8481
|
+
.then(function (serializedRecording) {
|
|
8482
|
+
return isRecordingExpired(serializedRecording) ? null : serializedRecording;
|
|
8483
|
+
}.bind(this))
|
|
8484
|
+
.catch(this.handleError.bind(this));
|
|
8485
|
+
};
|
|
8486
|
+
|
|
8487
|
+
RecordingRegistry.prototype.clearActiveRecording = function () {
|
|
8488
|
+
// mark recording as expired instead of deleting it in case the page unloads mid-flush and doesn't make it to ingestion.
|
|
8489
|
+
// this will ensure the next pageload will flush the remaining events, but not try to continue the recording.
|
|
8490
|
+
return this.getActiveRecording()
|
|
8491
|
+
.then(function (serializedRecording) {
|
|
8492
|
+
if (serializedRecording) {
|
|
8493
|
+
serializedRecording['maxExpires'] = 0;
|
|
8494
|
+
return this.setActiveRecording(serializedRecording);
|
|
8495
|
+
}
|
|
8496
|
+
}.bind(this))
|
|
8497
|
+
.catch(this.handleError.bind(this));
|
|
8498
|
+
};
|
|
8499
|
+
|
|
8500
|
+
/**
|
|
8501
|
+
* Flush any inactive recordings from the registry to minimize data loss.
|
|
8502
|
+
* The main idea here is that we can flush remaining rrweb events on the next page load if a tab is closed mid-batch.
|
|
8503
|
+
*/
|
|
8504
|
+
RecordingRegistry.prototype.flushInactiveRecordings = function () {
|
|
8505
|
+
return this.idb.init()
|
|
8506
|
+
.then(function() {
|
|
8507
|
+
return this.idb.getAll();
|
|
8508
|
+
}.bind(this))
|
|
8509
|
+
.then(function (serializedRecordings) {
|
|
8510
|
+
// clean up any expired recordings from the registry, non-expired ones may be active in other tabs
|
|
8511
|
+
var unloadPromises = serializedRecordings
|
|
8512
|
+
.filter(function (serializedRecording) {
|
|
8513
|
+
return isRecordingExpired(serializedRecording);
|
|
8514
|
+
})
|
|
8515
|
+
.map(function (serializedRecording) {
|
|
8516
|
+
var sessionRecording = SessionRecording.deserialize(serializedRecording, {
|
|
8517
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8518
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8519
|
+
});
|
|
8520
|
+
return sessionRecording.unloadPersistedData()
|
|
8521
|
+
.then(function () {
|
|
8522
|
+
// expired recording was successfully flushed, we can clean it up from the registry
|
|
8523
|
+
return this.idb.removeItem(serializedRecording['tabId']);
|
|
8524
|
+
}.bind(this))
|
|
8525
|
+
.catch(this.handleError.bind(this));
|
|
8526
|
+
}.bind(this));
|
|
8527
|
+
|
|
8528
|
+
return PromisePolyfill.all(unloadPromises);
|
|
8529
|
+
}.bind(this))
|
|
8530
|
+
.catch(this.handleError.bind(this));
|
|
8531
|
+
};
|
|
8532
|
+
|
|
8129
8533
|
var logger$1 = console_with_prefix('recorder');
|
|
8130
8534
|
|
|
8131
8535
|
/**
|
|
8132
|
-
* Recorder API:
|
|
8536
|
+
* Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
|
|
8133
8537
|
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
8134
|
-
|
|
8135
|
-
var MixpanelRecorder = function(mixpanelInstance) {
|
|
8136
|
-
this.
|
|
8538
|
+
*/
|
|
8539
|
+
var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage) {
|
|
8540
|
+
this.mixpanelInstance = mixpanelInstance;
|
|
8541
|
+
this.rrwebRecord = rrwebRecord || record;
|
|
8542
|
+
this.sharedLockStorage = sharedLockStorage;
|
|
8543
|
+
|
|
8544
|
+
/**
|
|
8545
|
+
* @member {import('./registry').RecordingRegistry}
|
|
8546
|
+
*/
|
|
8547
|
+
this.recordingRegistry = new RecordingRegistry({
|
|
8548
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8549
|
+
errorReporter: logger$1.error,
|
|
8550
|
+
sharedLockStorage: sharedLockStorage
|
|
8551
|
+
});
|
|
8552
|
+
this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
|
|
8553
|
+
|
|
8137
8554
|
this.activeRecording = null;
|
|
8138
8555
|
};
|
|
8139
8556
|
|
|
8140
|
-
MixpanelRecorder.prototype.startRecording = function(
|
|
8557
|
+
MixpanelRecorder.prototype.startRecording = function(options) {
|
|
8558
|
+
options = options || {};
|
|
8141
8559
|
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
8142
8560
|
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
8143
8561
|
return;
|
|
8144
8562
|
}
|
|
8145
8563
|
|
|
8146
|
-
var onIdleTimeout =
|
|
8564
|
+
var onIdleTimeout = function () {
|
|
8147
8565
|
logger$1.log('Idle timeout reached, restarting recording.');
|
|
8148
8566
|
this.resetRecording();
|
|
8149
|
-
}
|
|
8567
|
+
}.bind(this);
|
|
8150
8568
|
|
|
8151
|
-
var onMaxLengthReached =
|
|
8569
|
+
var onMaxLengthReached = function () {
|
|
8152
8570
|
logger$1.log('Max recording length reached, stopping recording.');
|
|
8153
8571
|
this.resetRecording();
|
|
8154
|
-
}
|
|
8572
|
+
}.bind(this);
|
|
8573
|
+
|
|
8574
|
+
var onBatchSent = function () {
|
|
8575
|
+
this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8576
|
+
this['__flushPromise'] = this.activeRecording.batcher._flushPromise;
|
|
8577
|
+
}.bind(this);
|
|
8155
8578
|
|
|
8156
|
-
|
|
8157
|
-
|
|
8579
|
+
/**
|
|
8580
|
+
* @type {import('./session-recording').SessionRecordingOptions}
|
|
8581
|
+
*/
|
|
8582
|
+
var sessionRecordingOptions = {
|
|
8583
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8584
|
+
onBatchSent: onBatchSent,
|
|
8158
8585
|
onIdleTimeout: onIdleTimeout,
|
|
8159
8586
|
onMaxLengthReached: onMaxLengthReached,
|
|
8160
8587
|
replayId: _.UUID(),
|
|
8161
|
-
rrwebRecord:
|
|
8162
|
-
|
|
8588
|
+
rrwebRecord: this.rrwebRecord,
|
|
8589
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8590
|
+
};
|
|
8591
|
+
|
|
8592
|
+
if (options.activeSerializedRecording) {
|
|
8593
|
+
this.activeRecording = SessionRecording.deserialize(options.activeSerializedRecording, sessionRecordingOptions);
|
|
8594
|
+
} else {
|
|
8595
|
+
this.activeRecording = new SessionRecording(sessionRecordingOptions);
|
|
8596
|
+
}
|
|
8163
8597
|
|
|
8164
|
-
this.activeRecording.startRecording(shouldStopBatcher);
|
|
8598
|
+
this.activeRecording.startRecording(options.shouldStopBatcher);
|
|
8599
|
+
return this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8165
8600
|
};
|
|
8166
8601
|
|
|
8167
8602
|
MixpanelRecorder.prototype.stopRecording = function() {
|
|
8603
|
+
var stopPromise = this._stopCurrentRecording(false);
|
|
8604
|
+
this.recordingRegistry.clearActiveRecording();
|
|
8605
|
+
this.activeRecording = null;
|
|
8606
|
+
return stopPromise;
|
|
8607
|
+
};
|
|
8608
|
+
|
|
8609
|
+
MixpanelRecorder.prototype.pauseRecording = function() {
|
|
8610
|
+
return this._stopCurrentRecording(false);
|
|
8611
|
+
};
|
|
8612
|
+
|
|
8613
|
+
MixpanelRecorder.prototype._stopCurrentRecording = function(skipFlush) {
|
|
8168
8614
|
if (this.activeRecording) {
|
|
8169
|
-
this.activeRecording.stopRecording();
|
|
8170
|
-
this.activeRecording = null;
|
|
8615
|
+
return this.activeRecording.stopRecording(skipFlush);
|
|
8171
8616
|
}
|
|
8617
|
+
return PromisePolyfill.resolve();
|
|
8172
8618
|
};
|
|
8173
8619
|
|
|
8620
|
+
MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
|
|
8621
|
+
if (this.activeRecording && this.activeRecording.isRrwebStopped()) {
|
|
8622
|
+
this.activeRecording.startRecording(false);
|
|
8623
|
+
return PromisePolyfill.resolve(null);
|
|
8624
|
+
}
|
|
8625
|
+
|
|
8626
|
+
return this.recordingRegistry.getActiveRecording()
|
|
8627
|
+
.then(function (activeSerializedRecording) {
|
|
8628
|
+
if (activeSerializedRecording) {
|
|
8629
|
+
return this.startRecording({activeSerializedRecording: activeSerializedRecording});
|
|
8630
|
+
} else if (startNewIfInactive) {
|
|
8631
|
+
return this.startRecording({shouldStopBatcher: false});
|
|
8632
|
+
} else {
|
|
8633
|
+
logger$1.log('No resumable recording found.');
|
|
8634
|
+
return null;
|
|
8635
|
+
}
|
|
8636
|
+
}.bind(this));
|
|
8637
|
+
};
|
|
8638
|
+
|
|
8639
|
+
|
|
8174
8640
|
MixpanelRecorder.prototype.resetRecording = function () {
|
|
8175
8641
|
this.stopRecording();
|
|
8176
|
-
this.startRecording(true);
|
|
8642
|
+
this.startRecording({shouldStopBatcher: true});
|
|
8177
8643
|
};
|
|
8178
8644
|
|
|
8179
8645
|
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
@@ -10389,11 +10855,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
|
|
|
10389
10855
|
* Released under the MIT License.
|
|
10390
10856
|
*/
|
|
10391
10857
|
|
|
10392
|
-
// ==ClosureCompiler==
|
|
10393
|
-
// @compilation_level ADVANCED_OPTIMIZATIONS
|
|
10394
|
-
// @output_file_name mixpanel-2.8.min.js
|
|
10395
|
-
// ==/ClosureCompiler==
|
|
10396
|
-
|
|
10397
10858
|
/*
|
|
10398
10859
|
SIMPLE STYLE GUIDE:
|
|
10399
10860
|
|
|
@@ -10416,7 +10877,6 @@ var INIT_MODULE = 0;
|
|
|
10416
10877
|
var INIT_SNIPPET = 1;
|
|
10417
10878
|
|
|
10418
10879
|
var IDENTITY_FUNC = function(x) {return x;};
|
|
10419
|
-
var NOOP_FUNC = function() {};
|
|
10420
10880
|
|
|
10421
10881
|
/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
|
|
10422
10882
|
/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
|
|
@@ -10725,34 +11185,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
10725
11185
|
this.autocapture = new Autocapture(this);
|
|
10726
11186
|
this.autocapture.init();
|
|
10727
11187
|
|
|
10728
|
-
|
|
10729
|
-
|
|
11188
|
+
this._init_tab_id();
|
|
11189
|
+
this._check_and_start_session_recording();
|
|
11190
|
+
};
|
|
11191
|
+
|
|
11192
|
+
/**
|
|
11193
|
+
* Assigns a unique UUID to this tab / window by leveraging sessionStorage.
|
|
11194
|
+
* This is primarily used for session recording, where data must be isolated to the current tab.
|
|
11195
|
+
*/
|
|
11196
|
+
MixpanelLib.prototype._init_tab_id = function() {
|
|
11197
|
+
if (_.sessionStorage.is_supported()) {
|
|
11198
|
+
try {
|
|
11199
|
+
var key_suffix = this.get_config('name') + '_' + this.get_config('token');
|
|
11200
|
+
var tab_id_key = 'mp_tab_id_' + key_suffix;
|
|
11201
|
+
|
|
11202
|
+
// A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
|
|
11203
|
+
// This enforces a unique ID in the cases like duplicated tab, window.open(...)
|
|
11204
|
+
var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
|
|
11205
|
+
if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
|
|
11206
|
+
_.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
|
|
11207
|
+
}
|
|
11208
|
+
|
|
11209
|
+
_.sessionStorage.set(should_generate_new_tab_id_key, '1');
|
|
11210
|
+
this.tab_id = _.sessionStorage.get(tab_id_key);
|
|
11211
|
+
|
|
11212
|
+
// 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,
|
|
11213
|
+
// but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
|
|
11214
|
+
// If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
|
|
11215
|
+
win.addEventListener('beforeunload', function () {
|
|
11216
|
+
_.sessionStorage.remove(should_generate_new_tab_id_key);
|
|
11217
|
+
});
|
|
11218
|
+
} catch(err) {
|
|
11219
|
+
this.report_error('Error initializing tab id', err);
|
|
11220
|
+
}
|
|
11221
|
+
} else {
|
|
11222
|
+
this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
|
|
10730
11223
|
}
|
|
10731
11224
|
};
|
|
10732
11225
|
|
|
10733
|
-
MixpanelLib.prototype.
|
|
11226
|
+
MixpanelLib.prototype.get_tab_id = function () {
|
|
11227
|
+
return this.tab_id || null;
|
|
11228
|
+
};
|
|
11229
|
+
|
|
11230
|
+
MixpanelLib.prototype._should_load_recorder = function () {
|
|
11231
|
+
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
11232
|
+
var tab_id = this.get_tab_id();
|
|
11233
|
+
return recording_registry_idb.init()
|
|
11234
|
+
.then(function () {
|
|
11235
|
+
return recording_registry_idb.getAll();
|
|
11236
|
+
})
|
|
11237
|
+
.then(function (recordings) {
|
|
11238
|
+
for (var i = 0; i < recordings.length; i++) {
|
|
11239
|
+
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
11240
|
+
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
11241
|
+
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
11242
|
+
return true;
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
return false;
|
|
11246
|
+
})
|
|
11247
|
+
.catch(_.bind(function (err) {
|
|
11248
|
+
this.report_error('Error checking recording registry', err);
|
|
11249
|
+
}, this));
|
|
11250
|
+
};
|
|
11251
|
+
|
|
11252
|
+
MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
|
|
10734
11253
|
if (!win['MutationObserver']) {
|
|
10735
11254
|
console$1.critical('Browser does not support MutationObserver; skipping session recording');
|
|
10736
11255
|
return;
|
|
10737
11256
|
}
|
|
10738
11257
|
|
|
10739
|
-
var
|
|
10740
|
-
|
|
10741
|
-
|
|
11258
|
+
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
11259
|
+
var handleLoadedRecorder = _.bind(function() {
|
|
11260
|
+
this._recorder = this._recorder || new win['__mp_recorder'](this);
|
|
11261
|
+
this._recorder['resumeRecording'](startNewIfInactive);
|
|
11262
|
+
}, this);
|
|
11263
|
+
|
|
11264
|
+
if (_.isUndefined(win['__mp_recorder'])) {
|
|
11265
|
+
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
11266
|
+
} else {
|
|
11267
|
+
handleLoadedRecorder();
|
|
11268
|
+
}
|
|
10742
11269
|
}, this);
|
|
10743
11270
|
|
|
10744
|
-
|
|
10745
|
-
|
|
11271
|
+
/**
|
|
11272
|
+
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
11273
|
+
* 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.
|
|
11274
|
+
*/
|
|
11275
|
+
var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
|
|
11276
|
+
if (force_start || is_sampled) {
|
|
11277
|
+
loadRecorder(true);
|
|
10746
11278
|
} else {
|
|
10747
|
-
|
|
11279
|
+
this._should_load_recorder()
|
|
11280
|
+
.then(function (shouldLoad) {
|
|
11281
|
+
if (shouldLoad) {
|
|
11282
|
+
loadRecorder(false);
|
|
11283
|
+
}
|
|
11284
|
+
});
|
|
10748
11285
|
}
|
|
10749
11286
|
});
|
|
10750
11287
|
|
|
11288
|
+
MixpanelLib.prototype.start_session_recording = function () {
|
|
11289
|
+
this._check_and_start_session_recording(true);
|
|
11290
|
+
};
|
|
11291
|
+
|
|
10751
11292
|
MixpanelLib.prototype.stop_session_recording = function () {
|
|
10752
11293
|
if (this._recorder) {
|
|
10753
11294
|
this._recorder['stopRecording']();
|
|
10754
|
-
}
|
|
10755
|
-
|
|
11295
|
+
}
|
|
11296
|
+
};
|
|
11297
|
+
|
|
11298
|
+
MixpanelLib.prototype.pause_session_recording = function () {
|
|
11299
|
+
if (this._recorder) {
|
|
11300
|
+
this._recorder['pauseRecording']();
|
|
11301
|
+
}
|
|
11302
|
+
};
|
|
11303
|
+
|
|
11304
|
+
MixpanelLib.prototype.resume_session_recording = function () {
|
|
11305
|
+
if (this._recorder) {
|
|
11306
|
+
this._recorder['resumeRecording']();
|
|
10756
11307
|
}
|
|
10757
11308
|
};
|
|
10758
11309
|
|
|
@@ -10787,6 +11338,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
|
|
|
10787
11338
|
return replay_id || null;
|
|
10788
11339
|
};
|
|
10789
11340
|
|
|
11341
|
+
// "private" public method to reach into the recorder in test cases
|
|
11342
|
+
MixpanelLib.prototype.__get_recorder = function () {
|
|
11343
|
+
return this._recorder;
|
|
11344
|
+
};
|
|
11345
|
+
|
|
10790
11346
|
// Private methods
|
|
10791
11347
|
|
|
10792
11348
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -11126,7 +11682,8 @@ MixpanelLib.prototype.init_batchers = function() {
|
|
|
11126
11682
|
return this._run_hook('before_send_' + attrs.type, item);
|
|
11127
11683
|
}, this),
|
|
11128
11684
|
stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
|
|
11129
|
-
usePersistence: true
|
|
11685
|
+
usePersistence: true,
|
|
11686
|
+
enqueueThrottleMs: 10,
|
|
11130
11687
|
}
|
|
11131
11688
|
);
|
|
11132
11689
|
}, this);
|
|
@@ -12227,6 +12784,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
|
|
|
12227
12784
|
|
|
12228
12785
|
if (disabled) {
|
|
12229
12786
|
this.stop_batch_senders();
|
|
12787
|
+
this.stop_session_recording();
|
|
12230
12788
|
} else {
|
|
12231
12789
|
// only start batchers after opt-in if they have previously been started
|
|
12232
12790
|
// in order to avoid unintentionally starting up batching for the first time
|
|
@@ -12467,10 +13025,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
|
|
|
12467
13025
|
MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
|
|
12468
13026
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
12469
13027
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
13028
|
+
MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
|
|
13029
|
+
MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
|
|
12470
13030
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
12471
13031
|
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
13032
|
+
MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
|
|
12472
13033
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
12473
13034
|
|
|
13035
|
+
// Exports intended only for testing
|
|
13036
|
+
MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
|
|
13037
|
+
|
|
12474
13038
|
// MixpanelPersistence Exports
|
|
12475
13039
|
MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
|
|
12476
13040
|
MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;
|