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