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