mixpanel-browser 2.59.0 → 2.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -1
- package/README.md +3 -3
- package/dist/mixpanel-core.cjs.js +612 -176
- package/dist/mixpanel-recorder.js +670 -224
- package/dist/mixpanel-recorder.min.js +11 -11
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +612 -176
- package/dist/mixpanel.amd.js +1000 -290
- package/dist/mixpanel.cjs.js +1000 -290
- package/dist/mixpanel.globals.js +612 -176
- package/dist/mixpanel.min.js +143 -134
- package/dist/mixpanel.module.js +1000 -290
- package/dist/mixpanel.umd.js +1000 -290
- package/package.json +2 -1
- package/src/autocapture/index.js +80 -9
- package/src/autocapture/utils.js +129 -38
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +119 -19
- package/src/mixpanel-persistence.js +6 -2
- package/src/recorder/index.js +1 -70
- package/src/recorder/recorder.js +137 -0
- package/src/recorder/recording-registry.js +98 -0
- package/src/recorder/session-recording.js +162 -43
- package/src/recorder/utils.js +12 -0
- package/src/request-batcher.js +6 -2
- package/src/request-queue.js +45 -39
- package/src/shared-lock.js +1 -1
- package/src/storage/indexed-db.js +127 -0
- package/src/storage/local-storage.js +4 -8
- package/src/storage/wrapper.js +3 -3
- package/src/utils.js +99 -61
package/dist/mixpanel.module.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
2
|
+
var win;
|
|
3
|
+
if (typeof(window) === 'undefined') {
|
|
4
|
+
var loc = {
|
|
5
|
+
hostname: ''
|
|
6
|
+
};
|
|
7
|
+
win = {
|
|
8
|
+
navigator: { userAgent: '', onLine: true },
|
|
9
|
+
document: {
|
|
10
|
+
createElement: function() { return {}; },
|
|
11
|
+
location: loc,
|
|
12
|
+
referrer: ''
|
|
13
|
+
},
|
|
14
|
+
screen: { width: 0, height: 0 },
|
|
15
|
+
location: loc,
|
|
16
|
+
addEventListener: function() {},
|
|
17
|
+
removeEventListener: function() {}
|
|
18
|
+
};
|
|
19
|
+
} else {
|
|
20
|
+
win = window;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
var NodeType;
|
|
2
24
|
(function (NodeType) {
|
|
3
25
|
NodeType[NodeType["Document"] = 0] = "Document";
|
|
@@ -4474,64 +4496,6 @@ record.takeFullSnapshot = (isCheckout) => {
|
|
|
4474
4496
|
};
|
|
4475
4497
|
record.mirror = mirror;
|
|
4476
4498
|
|
|
4477
|
-
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
4478
|
-
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
|
|
4479
|
-
EventType2[EventType2["Load"] = 1] = "Load";
|
|
4480
|
-
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
|
|
4481
|
-
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
|
|
4482
|
-
EventType2[EventType2["Meta"] = 4] = "Meta";
|
|
4483
|
-
EventType2[EventType2["Custom"] = 5] = "Custom";
|
|
4484
|
-
EventType2[EventType2["Plugin"] = 6] = "Plugin";
|
|
4485
|
-
return EventType2;
|
|
4486
|
-
})(EventType || {});
|
|
4487
|
-
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
4488
|
-
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
|
|
4489
|
-
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
|
|
4490
|
-
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
|
|
4491
|
-
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
|
|
4492
|
-
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
|
|
4493
|
-
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
|
|
4494
|
-
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
|
|
4495
|
-
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
|
|
4496
|
-
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
|
|
4497
|
-
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
|
|
4498
|
-
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
|
|
4499
|
-
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
|
|
4500
|
-
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
|
|
4501
|
-
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
|
|
4502
|
-
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
|
|
4503
|
-
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
|
|
4504
|
-
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
|
|
4505
|
-
return IncrementalSource2;
|
|
4506
|
-
})(IncrementalSource || {});
|
|
4507
|
-
|
|
4508
|
-
var Config = {
|
|
4509
|
-
DEBUG: false,
|
|
4510
|
-
LIB_VERSION: '2.59.0'
|
|
4511
|
-
};
|
|
4512
|
-
|
|
4513
|
-
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
4514
|
-
var win;
|
|
4515
|
-
if (typeof(window) === 'undefined') {
|
|
4516
|
-
var loc = {
|
|
4517
|
-
hostname: ''
|
|
4518
|
-
};
|
|
4519
|
-
win = {
|
|
4520
|
-
navigator: { userAgent: '', onLine: true },
|
|
4521
|
-
document: {
|
|
4522
|
-
createElement: function() { return {}; },
|
|
4523
|
-
location: loc,
|
|
4524
|
-
referrer: ''
|
|
4525
|
-
},
|
|
4526
|
-
screen: { width: 0, height: 0 },
|
|
4527
|
-
location: loc,
|
|
4528
|
-
addEventListener: function() {},
|
|
4529
|
-
removeEventListener: function() {}
|
|
4530
|
-
};
|
|
4531
|
-
} else {
|
|
4532
|
-
win = window;
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
4499
|
var setImmediate = win['setImmediate'];
|
|
4536
4500
|
var builtInProp, cycle, schedulingQueue,
|
|
4537
4501
|
ToString = Object.prototype.toString,
|
|
@@ -4894,6 +4858,42 @@ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]'
|
|
|
4894
4858
|
PromisePolyfill = NpoPromise;
|
|
4895
4859
|
}
|
|
4896
4860
|
|
|
4861
|
+
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
4862
|
+
EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
|
|
4863
|
+
EventType2[EventType2["Load"] = 1] = "Load";
|
|
4864
|
+
EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
|
|
4865
|
+
EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
|
|
4866
|
+
EventType2[EventType2["Meta"] = 4] = "Meta";
|
|
4867
|
+
EventType2[EventType2["Custom"] = 5] = "Custom";
|
|
4868
|
+
EventType2[EventType2["Plugin"] = 6] = "Plugin";
|
|
4869
|
+
return EventType2;
|
|
4870
|
+
})(EventType || {});
|
|
4871
|
+
var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
|
|
4872
|
+
IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
|
|
4873
|
+
IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
|
|
4874
|
+
IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
|
|
4875
|
+
IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
|
|
4876
|
+
IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
|
|
4877
|
+
IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
|
|
4878
|
+
IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
|
|
4879
|
+
IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
|
|
4880
|
+
IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
|
|
4881
|
+
IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
|
|
4882
|
+
IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
|
|
4883
|
+
IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
|
|
4884
|
+
IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
|
|
4885
|
+
IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
|
|
4886
|
+
IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
|
|
4887
|
+
IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
|
|
4888
|
+
IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
|
|
4889
|
+
return IncrementalSource2;
|
|
4890
|
+
})(IncrementalSource || {});
|
|
4891
|
+
|
|
4892
|
+
var Config = {
|
|
4893
|
+
DEBUG: false,
|
|
4894
|
+
LIB_VERSION: '2.61.0'
|
|
4895
|
+
};
|
|
4896
|
+
|
|
4897
4897
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
4898
4898
|
|
|
4899
4899
|
// Maximum allowed session recording length
|
|
@@ -5973,15 +5973,9 @@ _.cookie = {
|
|
|
5973
5973
|
}
|
|
5974
5974
|
};
|
|
5975
5975
|
|
|
5976
|
-
var
|
|
5977
|
-
var localStorageSupported = function(storage, forceCheck) {
|
|
5978
|
-
if (_localStorageSupported !== null && !forceCheck) {
|
|
5979
|
-
return _localStorageSupported;
|
|
5980
|
-
}
|
|
5981
|
-
|
|
5976
|
+
var _testStorageSupported = function (storage) {
|
|
5982
5977
|
var supported = true;
|
|
5983
5978
|
try {
|
|
5984
|
-
storage = storage || win.localStorage;
|
|
5985
5979
|
var key = '__mplss_' + cheap_guid(8),
|
|
5986
5980
|
val = 'xyz';
|
|
5987
5981
|
storage.setItem(key, val);
|
|
@@ -5992,59 +5986,74 @@ var localStorageSupported = function(storage, forceCheck) {
|
|
|
5992
5986
|
} catch (err) {
|
|
5993
5987
|
supported = false;
|
|
5994
5988
|
}
|
|
5995
|
-
|
|
5996
|
-
_localStorageSupported = supported;
|
|
5997
5989
|
return supported;
|
|
5998
5990
|
};
|
|
5999
5991
|
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
return supported;
|
|
6008
|
-
},
|
|
6009
|
-
|
|
6010
|
-
error: function(msg) {
|
|
6011
|
-
console$1.error('localStorage error: ' + msg);
|
|
6012
|
-
},
|
|
5992
|
+
var _localStorageSupported = null;
|
|
5993
|
+
var localStorageSupported = function(storage, forceCheck) {
|
|
5994
|
+
if (_localStorageSupported !== null && !forceCheck) {
|
|
5995
|
+
return _localStorageSupported;
|
|
5996
|
+
}
|
|
5997
|
+
return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
|
|
5998
|
+
};
|
|
6013
5999
|
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
},
|
|
6000
|
+
var _sessionStorageSupported = null;
|
|
6001
|
+
var sessionStorageSupported = function(storage, forceCheck) {
|
|
6002
|
+
if (_sessionStorageSupported !== null && !forceCheck) {
|
|
6003
|
+
return _sessionStorageSupported;
|
|
6004
|
+
}
|
|
6005
|
+
return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
|
|
6006
|
+
};
|
|
6022
6007
|
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
// noop
|
|
6028
|
-
}
|
|
6029
|
-
return null;
|
|
6030
|
-
},
|
|
6008
|
+
function _storageWrapper(storage, name, is_supported_fn) {
|
|
6009
|
+
var log_error = function(msg) {
|
|
6010
|
+
console$1.error(name + ' error: ' + msg);
|
|
6011
|
+
};
|
|
6031
6012
|
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6013
|
+
return {
|
|
6014
|
+
is_supported: function(forceCheck) {
|
|
6015
|
+
var supported = is_supported_fn(storage, forceCheck);
|
|
6016
|
+
if (!supported) {
|
|
6017
|
+
console$1.error(name + ' unsupported');
|
|
6018
|
+
}
|
|
6019
|
+
return supported;
|
|
6020
|
+
},
|
|
6021
|
+
error: log_error,
|
|
6022
|
+
get: function(key) {
|
|
6023
|
+
try {
|
|
6024
|
+
return storage.getItem(key);
|
|
6025
|
+
} catch (err) {
|
|
6026
|
+
log_error(err);
|
|
6027
|
+
}
|
|
6028
|
+
return null;
|
|
6029
|
+
},
|
|
6030
|
+
parse: function(key) {
|
|
6031
|
+
try {
|
|
6032
|
+
return _.JSONDecode(storage.getItem(key)) || {};
|
|
6033
|
+
} catch (err) {
|
|
6034
|
+
// noop
|
|
6035
|
+
}
|
|
6036
|
+
return null;
|
|
6037
|
+
},
|
|
6038
|
+
set: function(key, value) {
|
|
6039
|
+
try {
|
|
6040
|
+
storage.setItem(key, value);
|
|
6041
|
+
} catch (err) {
|
|
6042
|
+
log_error(err);
|
|
6043
|
+
}
|
|
6044
|
+
},
|
|
6045
|
+
remove: function(key) {
|
|
6046
|
+
try {
|
|
6047
|
+
storage.removeItem(key);
|
|
6048
|
+
} catch (err) {
|
|
6049
|
+
log_error(err);
|
|
6050
|
+
}
|
|
6037
6051
|
}
|
|
6038
|
-
}
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6039
6054
|
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
win.localStorage.removeItem(name);
|
|
6043
|
-
} catch (err) {
|
|
6044
|
-
_.localStorage.error(err);
|
|
6045
|
-
}
|
|
6046
|
-
}
|
|
6047
|
-
};
|
|
6055
|
+
_.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
|
|
6056
|
+
_.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
|
|
6048
6057
|
|
|
6049
6058
|
_.register_event = (function() {
|
|
6050
6059
|
// written by Dean Edwards, 2005
|
|
@@ -6571,6 +6580,31 @@ _.info = {
|
|
|
6571
6580
|
}
|
|
6572
6581
|
};
|
|
6573
6582
|
|
|
6583
|
+
/**
|
|
6584
|
+
* Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
|
|
6585
|
+
* Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
|
|
6586
|
+
*/
|
|
6587
|
+
var batchedThrottle = function (fn, waitMs) {
|
|
6588
|
+
var timeoutPromise = null;
|
|
6589
|
+
var throttledItems = [];
|
|
6590
|
+
return function (item) {
|
|
6591
|
+
var self = this;
|
|
6592
|
+
throttledItems.push(item);
|
|
6593
|
+
|
|
6594
|
+
if (!timeoutPromise) {
|
|
6595
|
+
timeoutPromise = new PromisePolyfill(function (resolve) {
|
|
6596
|
+
setTimeout(function () {
|
|
6597
|
+
var returnValue = fn.apply(self, [throttledItems]);
|
|
6598
|
+
timeoutPromise = null;
|
|
6599
|
+
throttledItems = [];
|
|
6600
|
+
resolve(returnValue);
|
|
6601
|
+
}, waitMs);
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6604
|
+
return timeoutPromise;
|
|
6605
|
+
};
|
|
6606
|
+
};
|
|
6607
|
+
|
|
6574
6608
|
var cheap_guid = function(maxlen) {
|
|
6575
6609
|
var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
|
|
6576
6610
|
return maxlen ? guid.substring(0, maxlen) : guid;
|
|
@@ -6613,6 +6647,8 @@ var isOnline = function() {
|
|
|
6613
6647
|
return _.isUndefined(onLine) || onLine;
|
|
6614
6648
|
};
|
|
6615
6649
|
|
|
6650
|
+
var NOOP_FUNC = function () {};
|
|
6651
|
+
|
|
6616
6652
|
var JSONStringify = null, JSONParse = null;
|
|
6617
6653
|
if (typeof JSON !== 'undefined') {
|
|
6618
6654
|
JSONStringify = JSON.stringify;
|
|
@@ -6621,20 +6657,143 @@ if (typeof JSON !== 'undefined') {
|
|
|
6621
6657
|
JSONStringify = JSONStringify || _.JSONEncode;
|
|
6622
6658
|
JSONParse = JSONParse || _.JSONDecode;
|
|
6623
6659
|
|
|
6624
|
-
// EXPORTS (for closure compiler)
|
|
6625
|
-
_['toArray'] = _.toArray;
|
|
6626
|
-
_['isObject'] = _.isObject;
|
|
6627
|
-
_['JSONEncode'] = _.JSONEncode;
|
|
6628
|
-
_['JSONDecode'] = _.JSONDecode;
|
|
6629
|
-
_['isBlockedUA'] = _.isBlockedUA;
|
|
6630
|
-
_['isEmptyObject'] = _.isEmptyObject;
|
|
6660
|
+
// UNMINIFIED EXPORTS (for closure compiler)
|
|
6631
6661
|
_['info'] = _.info;
|
|
6632
|
-
_['info']['device'] = _.info.device;
|
|
6633
6662
|
_['info']['browser'] = _.info.browser;
|
|
6634
6663
|
_['info']['browserVersion'] = _.info.browserVersion;
|
|
6664
|
+
_['info']['device'] = _.info.device;
|
|
6635
6665
|
_['info']['properties'] = _.info.properties;
|
|
6666
|
+
_['isBlockedUA'] = _.isBlockedUA;
|
|
6667
|
+
_['isEmptyObject'] = _.isEmptyObject;
|
|
6668
|
+
_['isObject'] = _.isObject;
|
|
6669
|
+
_['JSONDecode'] = _.JSONDecode;
|
|
6670
|
+
_['JSONEncode'] = _.JSONEncode;
|
|
6671
|
+
_['toArray'] = _.toArray;
|
|
6636
6672
|
_['NPO'] = NpoPromise;
|
|
6637
6673
|
|
|
6674
|
+
var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
|
|
6675
|
+
|
|
6676
|
+
var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
|
|
6677
|
+
var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
|
|
6678
|
+
|
|
6679
|
+
// note: increment the version number when adding new object stores
|
|
6680
|
+
var DB_VERSION = 1;
|
|
6681
|
+
var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
|
|
6682
|
+
|
|
6683
|
+
/**
|
|
6684
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
6685
|
+
*/
|
|
6686
|
+
var IDBStorageWrapper = function (storeName) {
|
|
6687
|
+
/**
|
|
6688
|
+
* @type {Promise<IDBDatabase>|null}
|
|
6689
|
+
*/
|
|
6690
|
+
this.dbPromise = null;
|
|
6691
|
+
this.storeName = storeName;
|
|
6692
|
+
};
|
|
6693
|
+
|
|
6694
|
+
IDBStorageWrapper.prototype._openDb = function () {
|
|
6695
|
+
return new PromisePolyfill(function (resolve, reject) {
|
|
6696
|
+
var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
|
|
6697
|
+
openRequest['onerror'] = function () {
|
|
6698
|
+
reject(openRequest.error);
|
|
6699
|
+
};
|
|
6700
|
+
|
|
6701
|
+
openRequest['onsuccess'] = function () {
|
|
6702
|
+
resolve(openRequest.result);
|
|
6703
|
+
};
|
|
6704
|
+
|
|
6705
|
+
openRequest['onupgradeneeded'] = function (ev) {
|
|
6706
|
+
var db = ev.target.result;
|
|
6707
|
+
|
|
6708
|
+
OBJECT_STORES.forEach(function (storeName) {
|
|
6709
|
+
db.createObjectStore(storeName);
|
|
6710
|
+
});
|
|
6711
|
+
};
|
|
6712
|
+
});
|
|
6713
|
+
};
|
|
6714
|
+
|
|
6715
|
+
IDBStorageWrapper.prototype.init = function () {
|
|
6716
|
+
if (!win.indexedDB) {
|
|
6717
|
+
return PromisePolyfill.reject('indexedDB is not supported in this browser');
|
|
6718
|
+
}
|
|
6719
|
+
|
|
6720
|
+
if (!this.dbPromise) {
|
|
6721
|
+
this.dbPromise = this._openDb();
|
|
6722
|
+
}
|
|
6723
|
+
|
|
6724
|
+
return this.dbPromise
|
|
6725
|
+
.then(function (dbOrError) {
|
|
6726
|
+
if (dbOrError instanceof win['IDBDatabase']) {
|
|
6727
|
+
return PromisePolyfill.resolve();
|
|
6728
|
+
} else {
|
|
6729
|
+
return PromisePolyfill.reject(dbOrError);
|
|
6730
|
+
}
|
|
6731
|
+
});
|
|
6732
|
+
};
|
|
6733
|
+
|
|
6734
|
+
/**
|
|
6735
|
+
* @param {IDBTransactionMode} mode
|
|
6736
|
+
* @param {function(IDBObjectStore): void} storeCb
|
|
6737
|
+
*/
|
|
6738
|
+
IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
|
|
6739
|
+
var storeName = this.storeName;
|
|
6740
|
+
var doTransaction = function (db) {
|
|
6741
|
+
return new PromisePolyfill(function (resolve, reject) {
|
|
6742
|
+
var transaction = db.transaction(storeName, mode);
|
|
6743
|
+
transaction.oncomplete = function () {
|
|
6744
|
+
resolve(transaction);
|
|
6745
|
+
};
|
|
6746
|
+
transaction.onabort = transaction.onerror = function () {
|
|
6747
|
+
reject(transaction.error);
|
|
6748
|
+
};
|
|
6749
|
+
|
|
6750
|
+
storeCb(transaction.objectStore(storeName));
|
|
6751
|
+
});
|
|
6752
|
+
};
|
|
6753
|
+
|
|
6754
|
+
return this.dbPromise
|
|
6755
|
+
.then(doTransaction)
|
|
6756
|
+
.catch(function (err) {
|
|
6757
|
+
if (err['name'] === 'InvalidStateError') {
|
|
6758
|
+
// try reopening the DB if the connection is closed
|
|
6759
|
+
this.dbPromise = this._openDb();
|
|
6760
|
+
return this.dbPromise.then(doTransaction);
|
|
6761
|
+
} else {
|
|
6762
|
+
return PromisePolyfill.reject(err);
|
|
6763
|
+
}
|
|
6764
|
+
}.bind(this));
|
|
6765
|
+
};
|
|
6766
|
+
|
|
6767
|
+
IDBStorageWrapper.prototype.setItem = function (key, value) {
|
|
6768
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
6769
|
+
objectStore.put(value, key);
|
|
6770
|
+
});
|
|
6771
|
+
};
|
|
6772
|
+
|
|
6773
|
+
IDBStorageWrapper.prototype.getItem = function (key) {
|
|
6774
|
+
var req;
|
|
6775
|
+
return this.makeTransaction('readonly', function (objectStore) {
|
|
6776
|
+
req = objectStore.get(key);
|
|
6777
|
+
}).then(function () {
|
|
6778
|
+
return req.result;
|
|
6779
|
+
});
|
|
6780
|
+
};
|
|
6781
|
+
|
|
6782
|
+
IDBStorageWrapper.prototype.removeItem = function (key) {
|
|
6783
|
+
return this.makeTransaction('readwrite', function (objectStore) {
|
|
6784
|
+
objectStore.delete(key);
|
|
6785
|
+
});
|
|
6786
|
+
};
|
|
6787
|
+
|
|
6788
|
+
IDBStorageWrapper.prototype.getAll = function () {
|
|
6789
|
+
var req;
|
|
6790
|
+
return this.makeTransaction('readonly', function (objectStore) {
|
|
6791
|
+
req = objectStore.getAll();
|
|
6792
|
+
}).then(function () {
|
|
6793
|
+
return req.result;
|
|
6794
|
+
});
|
|
6795
|
+
};
|
|
6796
|
+
|
|
6638
6797
|
/**
|
|
6639
6798
|
* GDPR utils
|
|
6640
6799
|
*
|
|
@@ -6960,7 +7119,7 @@ var SharedLock = function(key, options) {
|
|
|
6960
7119
|
options = options || {};
|
|
6961
7120
|
|
|
6962
7121
|
this.storageKey = key;
|
|
6963
|
-
this.storage = options.storage ||
|
|
7122
|
+
this.storage = options.storage || win.localStorage;
|
|
6964
7123
|
this.pollIntervalMS = options.pollIntervalMS || 100;
|
|
6965
7124
|
this.timeoutMS = options.timeoutMS || 2000;
|
|
6966
7125
|
|
|
@@ -6975,7 +7134,6 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
|
|
|
6975
7134
|
return new Promise(_.bind(function (resolve, reject) {
|
|
6976
7135
|
var i = pid || (new Date().getTime() + '|' + Math.random());
|
|
6977
7136
|
var startTime = new Date().getTime();
|
|
6978
|
-
|
|
6979
7137
|
var key = this.storageKey;
|
|
6980
7138
|
var pollIntervalMS = this.pollIntervalMS;
|
|
6981
7139
|
var timeoutMS = this.timeoutMS;
|
|
@@ -7086,11 +7244,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
|
|
|
7086
7244
|
};
|
|
7087
7245
|
|
|
7088
7246
|
/**
|
|
7089
|
-
* @
|
|
7090
|
-
*/
|
|
7091
|
-
|
|
7092
|
-
/**
|
|
7093
|
-
* @type {StorageWrapper}
|
|
7247
|
+
* @type {import('./wrapper').StorageWrapper}
|
|
7094
7248
|
*/
|
|
7095
7249
|
var LocalStorageWrapper = function (storageOverride) {
|
|
7096
7250
|
this.storage = storageOverride || localStorage;
|
|
@@ -7103,7 +7257,7 @@ LocalStorageWrapper.prototype.init = function () {
|
|
|
7103
7257
|
LocalStorageWrapper.prototype.setItem = function (key, value) {
|
|
7104
7258
|
return new PromisePolyfill(_.bind(function (resolve, reject) {
|
|
7105
7259
|
try {
|
|
7106
|
-
this.storage.setItem(key, value);
|
|
7260
|
+
this.storage.setItem(key, JSONStringify(value));
|
|
7107
7261
|
} catch (e) {
|
|
7108
7262
|
reject(e);
|
|
7109
7263
|
}
|
|
@@ -7115,7 +7269,7 @@ LocalStorageWrapper.prototype.getItem = function (key) {
|
|
|
7115
7269
|
return new PromisePolyfill(_.bind(function (resolve, reject) {
|
|
7116
7270
|
var item;
|
|
7117
7271
|
try {
|
|
7118
|
-
item = this.storage.getItem(key);
|
|
7272
|
+
item = JSONParse(this.storage.getItem(key));
|
|
7119
7273
|
} catch (e) {
|
|
7120
7274
|
reject(e);
|
|
7121
7275
|
}
|
|
@@ -7158,8 +7312,10 @@ var RequestQueue = function (storageKey, options) {
|
|
|
7158
7312
|
this.usePersistence = options.usePersistence;
|
|
7159
7313
|
if (this.usePersistence) {
|
|
7160
7314
|
this.queueStorage = options.queueStorage || new LocalStorageWrapper();
|
|
7161
|
-
this.lock = new SharedLock(storageKey, {
|
|
7162
|
-
|
|
7315
|
+
this.lock = new SharedLock(storageKey, {
|
|
7316
|
+
storage: options.sharedLockStorage || win.localStorage,
|
|
7317
|
+
timeoutMS: options.sharedLockTimeoutMS,
|
|
7318
|
+
});
|
|
7163
7319
|
}
|
|
7164
7320
|
this.reportError = options.errorReporter || _.bind(logger$4.error, logger$4);
|
|
7165
7321
|
|
|
@@ -7167,6 +7323,14 @@ var RequestQueue = function (storageKey, options) {
|
|
|
7167
7323
|
|
|
7168
7324
|
this.memQueue = [];
|
|
7169
7325
|
this.initialized = false;
|
|
7326
|
+
|
|
7327
|
+
if (options.enqueueThrottleMs) {
|
|
7328
|
+
this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
|
|
7329
|
+
} else {
|
|
7330
|
+
this.enqueuePersisted = _.bind(function (queueEntry) {
|
|
7331
|
+
return this._enqueuePersisted([queueEntry]);
|
|
7332
|
+
}, this);
|
|
7333
|
+
}
|
|
7170
7334
|
};
|
|
7171
7335
|
|
|
7172
7336
|
RequestQueue.prototype.ensureInit = function () {
|
|
@@ -7209,36 +7373,39 @@ RequestQueue.prototype.enqueue = function (item, flushInterval) {
|
|
|
7209
7373
|
this.memQueue.push(queueEntry);
|
|
7210
7374
|
return PromisePolyfill.resolve(true);
|
|
7211
7375
|
} else {
|
|
7376
|
+
return this.enqueuePersisted(queueEntry);
|
|
7377
|
+
}
|
|
7378
|
+
};
|
|
7212
7379
|
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
return succeeded;
|
|
7228
|
-
}, this))
|
|
7229
|
-
.catch(_.bind(function (err) {
|
|
7230
|
-
this.reportError('Error enqueueing item', err, item);
|
|
7231
|
-
return false;
|
|
7232
|
-
}, this));
|
|
7233
|
-
}, this);
|
|
7380
|
+
RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
|
|
7381
|
+
var enqueueItem = _.bind(function () {
|
|
7382
|
+
return this.ensureInit()
|
|
7383
|
+
.then(_.bind(function () {
|
|
7384
|
+
return this.readFromStorage();
|
|
7385
|
+
}, this))
|
|
7386
|
+
.then(_.bind(function (storedQueue) {
|
|
7387
|
+
return this.saveToStorage(storedQueue.concat(queueEntries));
|
|
7388
|
+
}, this))
|
|
7389
|
+
.then(_.bind(function (succeeded) {
|
|
7390
|
+
// only add to in-memory queue when storage succeeds
|
|
7391
|
+
if (succeeded) {
|
|
7392
|
+
this.memQueue = this.memQueue.concat(queueEntries);
|
|
7393
|
+
}
|
|
7234
7394
|
|
|
7235
|
-
|
|
7236
|
-
|
|
7395
|
+
return succeeded;
|
|
7396
|
+
}, this))
|
|
7237
7397
|
.catch(_.bind(function (err) {
|
|
7238
|
-
this.reportError('Error
|
|
7398
|
+
this.reportError('Error enqueueing items', err, queueEntries);
|
|
7239
7399
|
return false;
|
|
7240
7400
|
}, this));
|
|
7241
|
-
}
|
|
7401
|
+
}, this);
|
|
7402
|
+
|
|
7403
|
+
return this.lock
|
|
7404
|
+
.withLock(enqueueItem, this.pid)
|
|
7405
|
+
.catch(_.bind(function (err) {
|
|
7406
|
+
this.reportError('Error acquiring storage lock', err);
|
|
7407
|
+
return false;
|
|
7408
|
+
}, this));
|
|
7242
7409
|
};
|
|
7243
7410
|
|
|
7244
7411
|
/**
|
|
@@ -7259,7 +7426,7 @@ RequestQueue.prototype.fillBatch = function (batchSize) {
|
|
|
7259
7426
|
}, this))
|
|
7260
7427
|
.then(_.bind(function (storedQueue) {
|
|
7261
7428
|
if (storedQueue.length) {
|
|
7262
|
-
|
|
7429
|
+
// item IDs already in batch; don't duplicate out of storage
|
|
7263
7430
|
var idsInBatch = {}; // poor man's Set
|
|
7264
7431
|
_.each(batch, function (item) {
|
|
7265
7432
|
idsInBatch[item['id']] = true;
|
|
@@ -7346,7 +7513,7 @@ RequestQueue.prototype.removeItemsByID = function (ids) {
|
|
|
7346
7513
|
.withLock(removeFromStorage, this.pid)
|
|
7347
7514
|
.catch(_.bind(function (err) {
|
|
7348
7515
|
this.reportError('Error acquiring storage lock', err);
|
|
7349
|
-
if (!localStorageSupported(this.
|
|
7516
|
+
if (!localStorageSupported(this.lock.storage, true)) {
|
|
7350
7517
|
// Looks like localStorage writes have stopped working sometime after
|
|
7351
7518
|
// initialization (probably full), and so nobody can acquire locks
|
|
7352
7519
|
// anymore. Consider it temporarily safe to remove items without the
|
|
@@ -7434,7 +7601,6 @@ RequestQueue.prototype.readFromStorage = function () {
|
|
|
7434
7601
|
}, this))
|
|
7435
7602
|
.then(_.bind(function (storageEntry) {
|
|
7436
7603
|
if (storageEntry) {
|
|
7437
|
-
storageEntry = JSONParse(storageEntry);
|
|
7438
7604
|
if (!_.isArray(storageEntry)) {
|
|
7439
7605
|
this.reportError('Invalid storage entry:', storageEntry);
|
|
7440
7606
|
storageEntry = null;
|
|
@@ -7452,16 +7618,9 @@ RequestQueue.prototype.readFromStorage = function () {
|
|
|
7452
7618
|
* Serialize the given items array to localStorage.
|
|
7453
7619
|
*/
|
|
7454
7620
|
RequestQueue.prototype.saveToStorage = function (queue) {
|
|
7455
|
-
try {
|
|
7456
|
-
var serialized = JSONStringify(queue);
|
|
7457
|
-
} catch (err) {
|
|
7458
|
-
this.reportError('Error serializing queue', err);
|
|
7459
|
-
return PromisePolyfill.resolve(false);
|
|
7460
|
-
}
|
|
7461
|
-
|
|
7462
7621
|
return this.ensureInit()
|
|
7463
7622
|
.then(_.bind(function () {
|
|
7464
|
-
return this.queueStorage.setItem(this.storageKey,
|
|
7623
|
+
return this.queueStorage.setItem(this.storageKey, queue);
|
|
7465
7624
|
}, this))
|
|
7466
7625
|
.then(function () {
|
|
7467
7626
|
return true;
|
|
@@ -7505,7 +7664,9 @@ var RequestBatcher = function(storageKey, options) {
|
|
|
7505
7664
|
errorReporter: _.bind(this.reportError, this),
|
|
7506
7665
|
queueStorage: options.queueStorage,
|
|
7507
7666
|
sharedLockStorage: options.sharedLockStorage,
|
|
7508
|
-
|
|
7667
|
+
sharedLockTimeoutMS: options.sharedLockTimeoutMS,
|
|
7668
|
+
usePersistence: options.usePersistence,
|
|
7669
|
+
enqueueThrottleMs: options.enqueueThrottleMs
|
|
7509
7670
|
});
|
|
7510
7671
|
|
|
7511
7672
|
this.libConfig = options.libConfig;
|
|
@@ -7527,6 +7688,8 @@ var RequestBatcher = function(storageKey, options) {
|
|
|
7527
7688
|
// as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
|
|
7528
7689
|
// in a request loop and get ratelimited by the server.
|
|
7529
7690
|
this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
|
|
7691
|
+
|
|
7692
|
+
this._flushPromise = null;
|
|
7530
7693
|
};
|
|
7531
7694
|
|
|
7532
7695
|
/**
|
|
@@ -7586,7 +7749,7 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
|
|
|
7586
7749
|
if (!this.stopped) { // don't schedule anymore if batching has been stopped
|
|
7587
7750
|
this.timeoutID = setTimeout(_.bind(function() {
|
|
7588
7751
|
if (!this.stopped) {
|
|
7589
|
-
this.flush();
|
|
7752
|
+
this._flushPromise = this.flush();
|
|
7590
7753
|
}
|
|
7591
7754
|
}, this), this.flushInterval);
|
|
7592
7755
|
}
|
|
@@ -7818,6 +7981,17 @@ RequestBatcher.prototype.reportError = function(msg, err) {
|
|
|
7818
7981
|
}
|
|
7819
7982
|
};
|
|
7820
7983
|
|
|
7984
|
+
/**
|
|
7985
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
7986
|
+
* @returns {boolean}
|
|
7987
|
+
*/
|
|
7988
|
+
var isRecordingExpired = function(serializedRecording) {
|
|
7989
|
+
var now = Date.now();
|
|
7990
|
+
return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
|
|
7991
|
+
};
|
|
7992
|
+
|
|
7993
|
+
var RECORD_ENQUEUE_THROTTLE_MS = 250;
|
|
7994
|
+
|
|
7821
7995
|
var logger$2 = console_with_prefix('recorder');
|
|
7822
7996
|
var CompressionStream = win['CompressionStream'];
|
|
7823
7997
|
|
|
@@ -7844,29 +8018,58 @@ function isUserEvent(ev) {
|
|
|
7844
8018
|
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
7845
8019
|
}
|
|
7846
8020
|
|
|
8021
|
+
/**
|
|
8022
|
+
* @typedef {Object} SerializedRecording
|
|
8023
|
+
* @property {number} idleExpires
|
|
8024
|
+
* @property {number} maxExpires
|
|
8025
|
+
* @property {number} replayStartTime
|
|
8026
|
+
* @property {number} seqNo
|
|
8027
|
+
* @property {string} batchStartUrl
|
|
8028
|
+
* @property {string} replayId
|
|
8029
|
+
* @property {string} tabId
|
|
8030
|
+
* @property {string} replayStartUrl
|
|
8031
|
+
*/
|
|
8032
|
+
|
|
8033
|
+
/**
|
|
8034
|
+
* @typedef {Object} SessionRecordingOptions
|
|
8035
|
+
* @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
8036
|
+
* @property {String} [options.replayId] - unique uuid for a single replay
|
|
8037
|
+
* @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
8038
|
+
* @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
8039
|
+
* @property {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
8040
|
+
* @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
|
|
8041
|
+
* @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
|
|
8042
|
+
* optional properties for deserialization:
|
|
8043
|
+
* @property {number} idleExpires
|
|
8044
|
+
* @property {number} maxExpires
|
|
8045
|
+
* @property {number} replayStartTime
|
|
8046
|
+
* @property {number} seqNo
|
|
8047
|
+
* @property {string} batchStartUrl
|
|
8048
|
+
* @property {string} replayStartUrl
|
|
8049
|
+
*/
|
|
8050
|
+
|
|
8051
|
+
|
|
7847
8052
|
/**
|
|
7848
8053
|
* This class encapsulates a single session recording and its lifecycle.
|
|
7849
|
-
* @param {
|
|
7850
|
-
* @param {String} [options.replayId] - unique uuid for a single replay
|
|
7851
|
-
* @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
7852
|
-
* @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
7853
|
-
* @param {Function} [options.rrwebRecord] - rrweb's `record` function
|
|
8054
|
+
* @param {SessionRecordingOptions} options
|
|
7854
8055
|
*/
|
|
7855
8056
|
var SessionRecording = function(options) {
|
|
7856
8057
|
this._mixpanel = options.mixpanelInstance;
|
|
7857
|
-
this._onIdleTimeout = options.onIdleTimeout;
|
|
7858
|
-
this._onMaxLengthReached = options.onMaxLengthReached;
|
|
7859
|
-
this.
|
|
7860
|
-
|
|
7861
|
-
this.replayId = options.replayId;
|
|
8058
|
+
this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
|
|
8059
|
+
this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
|
|
8060
|
+
this._onBatchSent = options.onBatchSent || NOOP_FUNC;
|
|
8061
|
+
this._rrwebRecord = options.rrwebRecord || null;
|
|
7862
8062
|
|
|
7863
8063
|
// internal rrweb stopRecording function
|
|
7864
8064
|
this._stopRecording = null;
|
|
8065
|
+
this.replayId = options.replayId;
|
|
7865
8066
|
|
|
7866
|
-
this.
|
|
7867
|
-
this.
|
|
7868
|
-
this.
|
|
7869
|
-
this.
|
|
8067
|
+
this.batchStartUrl = options.batchStartUrl || null;
|
|
8068
|
+
this.replayStartUrl = options.replayStartUrl || null;
|
|
8069
|
+
this.idleExpires = options.idleExpires || null;
|
|
8070
|
+
this.maxExpires = options.maxExpires || null;
|
|
8071
|
+
this.replayStartTime = options.replayStartTime || null;
|
|
8072
|
+
this.seqNo = options.seqNo || 0;
|
|
7870
8073
|
|
|
7871
8074
|
this.idleTimeoutId = null;
|
|
7872
8075
|
this.maxTimeoutId = null;
|
|
@@ -7874,18 +8077,40 @@ var SessionRecording = function(options) {
|
|
|
7874
8077
|
this.recordMaxMs = MAX_RECORDING_MS;
|
|
7875
8078
|
this.recordMinMs = 0;
|
|
7876
8079
|
|
|
8080
|
+
// disable persistence if localStorage is not supported
|
|
8081
|
+
// request-queue will automatically disable persistence if indexedDB fails to initialize
|
|
8082
|
+
var usePersistence = localStorageSupported(options.sharedLockStorage, true);
|
|
8083
|
+
|
|
7877
8084
|
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
7878
8085
|
// this will be important when persistence is introduced
|
|
7879
|
-
|
|
7880
|
-
this.
|
|
7881
|
-
|
|
8086
|
+
this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
|
|
8087
|
+
this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
|
|
8088
|
+
this.batcher = new RequestBatcher(this.batcherKey, {
|
|
8089
|
+
errorReporter: this.reportError.bind(this),
|
|
7882
8090
|
flushOnlyOnInterval: true,
|
|
7883
8091
|
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
7884
|
-
sendRequestFunc:
|
|
7885
|
-
|
|
8092
|
+
sendRequestFunc: this.flushEventsWithOptOut.bind(this),
|
|
8093
|
+
queueStorage: this.queueStorage,
|
|
8094
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
8095
|
+
usePersistence: usePersistence,
|
|
8096
|
+
stopAllBatchingFunc: this.stopRecording.bind(this),
|
|
8097
|
+
|
|
8098
|
+
// increased throttle and shared lock timeout because recording events are very high frequency.
|
|
8099
|
+
// this will minimize the amount of lock contention between enqueued events.
|
|
8100
|
+
// for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
|
|
8101
|
+
enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
|
|
8102
|
+
sharedLockTimeoutMS: 10 * 1000,
|
|
7886
8103
|
});
|
|
7887
8104
|
};
|
|
7888
8105
|
|
|
8106
|
+
SessionRecording.prototype.unloadPersistedData = function () {
|
|
8107
|
+
this.batcher.stop();
|
|
8108
|
+
return this.batcher.flush()
|
|
8109
|
+
.then(function () {
|
|
8110
|
+
return this.queueStorage.removeItem(this.batcherKey);
|
|
8111
|
+
}.bind(this));
|
|
8112
|
+
};
|
|
8113
|
+
|
|
7889
8114
|
SessionRecording.prototype.getConfig = function(configVar) {
|
|
7890
8115
|
return this._mixpanel.get_config(configVar);
|
|
7891
8116
|
};
|
|
@@ -7898,6 +8123,11 @@ SessionRecording.prototype.get_config = function(configVar) {
|
|
|
7898
8123
|
};
|
|
7899
8124
|
|
|
7900
8125
|
SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
8126
|
+
if (this._rrwebRecord === null) {
|
|
8127
|
+
this.reportError('rrweb record function not provided. ');
|
|
8128
|
+
return;
|
|
8129
|
+
}
|
|
8130
|
+
|
|
7901
8131
|
if (this._stopRecording !== null) {
|
|
7902
8132
|
logger$2.log('Recording already in progress, skipping startRecording.');
|
|
7903
8133
|
return;
|
|
@@ -7909,15 +8139,21 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7909
8139
|
logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
|
|
7910
8140
|
}
|
|
7911
8141
|
|
|
8142
|
+
if (!this.maxExpires) {
|
|
8143
|
+
this.maxExpires = new Date().getTime() + this.recordMaxMs;
|
|
8144
|
+
}
|
|
8145
|
+
|
|
7912
8146
|
this.recordMinMs = this.getConfig('record_min_ms');
|
|
7913
8147
|
if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
|
|
7914
8148
|
this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
|
|
7915
8149
|
logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
|
|
7916
8150
|
}
|
|
7917
8151
|
|
|
7918
|
-
this.replayStartTime
|
|
7919
|
-
|
|
7920
|
-
|
|
8152
|
+
if (!this.replayStartTime) {
|
|
8153
|
+
this.replayStartTime = new Date().getTime();
|
|
8154
|
+
this.batchStartUrl = _.info.currentUrl();
|
|
8155
|
+
this.replayStartUrl = _.info.currentUrl();
|
|
8156
|
+
}
|
|
7921
8157
|
|
|
7922
8158
|
if (shouldStopBatcher || this.recordMinMs > 0) {
|
|
7923
8159
|
// the primary case for shouldStopBatcher is when we're starting recording after a reset
|
|
@@ -7930,10 +8166,12 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7930
8166
|
this.batcher.start();
|
|
7931
8167
|
}
|
|
7932
8168
|
|
|
7933
|
-
var resetIdleTimeout =
|
|
8169
|
+
var resetIdleTimeout = function () {
|
|
7934
8170
|
clearTimeout(this.idleTimeoutId);
|
|
7935
|
-
|
|
7936
|
-
|
|
8171
|
+
var idleTimeoutMs = this.getConfig('record_idle_timeout_ms');
|
|
8172
|
+
this.idleTimeoutId = setTimeout(this._onIdleTimeout, idleTimeoutMs);
|
|
8173
|
+
this.idleExpires = new Date().getTime() + idleTimeoutMs;
|
|
8174
|
+
}.bind(this);
|
|
7937
8175
|
|
|
7938
8176
|
var blockSelector = this.getConfig('record_block_selector');
|
|
7939
8177
|
if (blockSelector === '' || blockSelector === null) {
|
|
@@ -7941,8 +8179,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7941
8179
|
}
|
|
7942
8180
|
|
|
7943
8181
|
this._stopRecording = this._rrwebRecord({
|
|
7944
|
-
'emit':
|
|
7945
|
-
this.batcher.enqueue(ev);
|
|
8182
|
+
'emit': addOptOutCheckMixpanelLib(function (ev) {
|
|
7946
8183
|
if (isUserEvent(ev)) {
|
|
7947
8184
|
if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
|
|
7948
8185
|
// start flushing again after user activity
|
|
@@ -7950,7 +8187,10 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7950
8187
|
}
|
|
7951
8188
|
resetIdleTimeout();
|
|
7952
8189
|
}
|
|
7953
|
-
|
|
8190
|
+
|
|
8191
|
+
// promise only used to await during tests
|
|
8192
|
+
this.__enqueuePromise = this.batcher.enqueue(ev);
|
|
8193
|
+
}.bind(this)),
|
|
7954
8194
|
'blockClass': this.getConfig('record_block_class'),
|
|
7955
8195
|
'blockSelector': blockSelector,
|
|
7956
8196
|
'collectFonts': this.getConfig('record_collect_fonts'),
|
|
@@ -7976,10 +8216,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
7976
8216
|
|
|
7977
8217
|
resetIdleTimeout();
|
|
7978
8218
|
|
|
7979
|
-
|
|
8219
|
+
var maxTimeoutMs = this.maxExpires - new Date().getTime();
|
|
8220
|
+
this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
|
|
7980
8221
|
};
|
|
7981
8222
|
|
|
7982
|
-
SessionRecording.prototype.stopRecording = function () {
|
|
8223
|
+
SessionRecording.prototype.stopRecording = function (skipFlush) {
|
|
7983
8224
|
if (!this.isRrwebStopped()) {
|
|
7984
8225
|
try {
|
|
7985
8226
|
this._stopRecording();
|
|
@@ -7989,17 +8230,19 @@ SessionRecording.prototype.stopRecording = function () {
|
|
|
7989
8230
|
this._stopRecording = null;
|
|
7990
8231
|
}
|
|
7991
8232
|
|
|
8233
|
+
var flushPromise;
|
|
7992
8234
|
if (this.batcher.stopped) {
|
|
7993
8235
|
// never got user activity to flush after reset, so just clear the batcher
|
|
7994
|
-
this.batcher.clear();
|
|
7995
|
-
} else {
|
|
8236
|
+
flushPromise = this.batcher.clear();
|
|
8237
|
+
} else if (!skipFlush) {
|
|
7996
8238
|
// flush any remaining events from running batcher
|
|
7997
|
-
this.batcher.flush();
|
|
7998
|
-
this.batcher.stop();
|
|
8239
|
+
flushPromise = this.batcher.flush();
|
|
7999
8240
|
}
|
|
8241
|
+
this.batcher.stop();
|
|
8000
8242
|
|
|
8001
8243
|
clearTimeout(this.idleTimeoutId);
|
|
8002
8244
|
clearTimeout(this.maxTimeoutId);
|
|
8245
|
+
return flushPromise;
|
|
8003
8246
|
};
|
|
8004
8247
|
|
|
8005
8248
|
SessionRecording.prototype.isRrwebStopped = function () {
|
|
@@ -8011,7 +8254,54 @@ SessionRecording.prototype.isRrwebStopped = function () {
|
|
|
8011
8254
|
* we stop recording and dump any queued events if the user has opted out.
|
|
8012
8255
|
*/
|
|
8013
8256
|
SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
8014
|
-
this._flushEvents(data, options, cb,
|
|
8257
|
+
this._flushEvents(data, options, cb, this._onOptOut.bind(this));
|
|
8258
|
+
};
|
|
8259
|
+
|
|
8260
|
+
/**
|
|
8261
|
+
* @returns {SerializedRecording}
|
|
8262
|
+
*/
|
|
8263
|
+
SessionRecording.prototype.serialize = function () {
|
|
8264
|
+
// don't break if mixpanel instance was destroyed at some point
|
|
8265
|
+
var tabId;
|
|
8266
|
+
try {
|
|
8267
|
+
tabId = this._mixpanel.get_tab_id();
|
|
8268
|
+
} catch (e) {
|
|
8269
|
+
this.reportError('Error getting tab ID for serialization ', e);
|
|
8270
|
+
tabId = null;
|
|
8271
|
+
}
|
|
8272
|
+
|
|
8273
|
+
return {
|
|
8274
|
+
'replayId': this.replayId,
|
|
8275
|
+
'seqNo': this.seqNo,
|
|
8276
|
+
'replayStartTime': this.replayStartTime,
|
|
8277
|
+
'batchStartUrl': this.batchStartUrl,
|
|
8278
|
+
'replayStartUrl': this.replayStartUrl,
|
|
8279
|
+
'idleExpires': this.idleExpires,
|
|
8280
|
+
'maxExpires': this.maxExpires,
|
|
8281
|
+
'tabId': tabId,
|
|
8282
|
+
};
|
|
8283
|
+
};
|
|
8284
|
+
|
|
8285
|
+
|
|
8286
|
+
/**
|
|
8287
|
+
* @static
|
|
8288
|
+
* @param {SerializedRecording} serializedRecording
|
|
8289
|
+
* @param {SessionRecordingOptions} options
|
|
8290
|
+
* @returns {SessionRecording}
|
|
8291
|
+
*/
|
|
8292
|
+
SessionRecording.deserialize = function (serializedRecording, options) {
|
|
8293
|
+
var recording = new SessionRecording(_.extend({}, options, {
|
|
8294
|
+
replayId: serializedRecording['replayId'],
|
|
8295
|
+
batchStartUrl: serializedRecording['batchStartUrl'],
|
|
8296
|
+
replayStartUrl: serializedRecording['replayStartUrl'],
|
|
8297
|
+
idleExpires: serializedRecording['idleExpires'],
|
|
8298
|
+
maxExpires: serializedRecording['maxExpires'],
|
|
8299
|
+
replayStartTime: serializedRecording['replayStartTime'],
|
|
8300
|
+
seqNo: serializedRecording['seqNo'],
|
|
8301
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
8302
|
+
}));
|
|
8303
|
+
|
|
8304
|
+
return recording;
|
|
8015
8305
|
};
|
|
8016
8306
|
|
|
8017
8307
|
SessionRecording.prototype._onOptOut = function (code) {
|
|
@@ -8022,7 +8312,7 @@ SessionRecording.prototype._onOptOut = function (code) {
|
|
|
8022
8312
|
};
|
|
8023
8313
|
|
|
8024
8314
|
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
8025
|
-
var onSuccess =
|
|
8315
|
+
var onSuccess = function (response, responseBody) {
|
|
8026
8316
|
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
8027
8317
|
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
8028
8318
|
// extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
|
|
@@ -8030,13 +8320,15 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
8030
8320
|
this.seqNo++;
|
|
8031
8321
|
this.batchStartUrl = _.info.currentUrl();
|
|
8032
8322
|
}
|
|
8323
|
+
|
|
8324
|
+
this._onBatchSent();
|
|
8033
8325
|
callback({
|
|
8034
8326
|
status: 0,
|
|
8035
8327
|
httpStatusCode: response.status,
|
|
8036
8328
|
responseBody: responseBody,
|
|
8037
8329
|
retryAfter: response.headers.get('Retry-After')
|
|
8038
8330
|
});
|
|
8039
|
-
}
|
|
8331
|
+
}.bind(this);
|
|
8040
8332
|
|
|
8041
8333
|
win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
8042
8334
|
'method': 'POST',
|
|
@@ -8057,7 +8349,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
|
|
|
8057
8349
|
};
|
|
8058
8350
|
|
|
8059
8351
|
SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
8060
|
-
|
|
8352
|
+
var numEvents = data.length;
|
|
8061
8353
|
|
|
8062
8354
|
if (numEvents > 0) {
|
|
8063
8355
|
var replayId = this.replayId;
|
|
@@ -8102,10 +8394,10 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
|
|
|
8102
8394
|
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
8103
8395
|
new Response(gzipStream)
|
|
8104
8396
|
.blob()
|
|
8105
|
-
.then(
|
|
8397
|
+
.then(function(compressedBlob) {
|
|
8106
8398
|
reqParams['format'] = 'gzip';
|
|
8107
8399
|
this._sendRequest(replayId, reqParams, compressedBlob, callback);
|
|
8108
|
-
}
|
|
8400
|
+
}.bind(this));
|
|
8109
8401
|
} else {
|
|
8110
8402
|
reqParams['format'] = 'body';
|
|
8111
8403
|
this._sendRequest(replayId, reqParams, eventsJson, callback);
|
|
@@ -8126,54 +8418,208 @@ SessionRecording.prototype.reportError = function(msg, err) {
|
|
|
8126
8418
|
}
|
|
8127
8419
|
};
|
|
8128
8420
|
|
|
8421
|
+
/**
|
|
8422
|
+
* Module for handling the storage and retrieval of recording metadata as well as any active recordings.
|
|
8423
|
+
* Makes sure that only one tab can be recording at a time.
|
|
8424
|
+
*/
|
|
8425
|
+
var RecordingRegistry = function (options) {
|
|
8426
|
+
this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
8427
|
+
this.errorReporter = options.errorReporter;
|
|
8428
|
+
this.mixpanelInstance = options.mixpanelInstance;
|
|
8429
|
+
this.sharedLockStorage = options.sharedLockStorage;
|
|
8430
|
+
};
|
|
8431
|
+
|
|
8432
|
+
RecordingRegistry.prototype.handleError = function (err) {
|
|
8433
|
+
this.errorReporter('IndexedDB error: ', err);
|
|
8434
|
+
};
|
|
8435
|
+
|
|
8436
|
+
/**
|
|
8437
|
+
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
8438
|
+
*/
|
|
8439
|
+
RecordingRegistry.prototype.setActiveRecording = function (serializedRecording) {
|
|
8440
|
+
var tabId = serializedRecording['tabId'];
|
|
8441
|
+
if (!tabId) {
|
|
8442
|
+
console.warn('No tab ID is set, cannot persist recording metadata.');
|
|
8443
|
+
return PromisePolyfill.resolve();
|
|
8444
|
+
}
|
|
8445
|
+
|
|
8446
|
+
return this.idb.init()
|
|
8447
|
+
.then(function () {
|
|
8448
|
+
return this.idb.setItem(tabId, serializedRecording);
|
|
8449
|
+
}.bind(this))
|
|
8450
|
+
.catch(this.handleError.bind(this));
|
|
8451
|
+
};
|
|
8452
|
+
|
|
8453
|
+
/**
|
|
8454
|
+
* @returns {Promise<import('./session-recording').SerializedRecording>}
|
|
8455
|
+
*/
|
|
8456
|
+
RecordingRegistry.prototype.getActiveRecording = function () {
|
|
8457
|
+
return this.idb.init()
|
|
8458
|
+
.then(function () {
|
|
8459
|
+
return this.idb.getItem(this.mixpanelInstance.get_tab_id());
|
|
8460
|
+
}.bind(this))
|
|
8461
|
+
.then(function (serializedRecording) {
|
|
8462
|
+
return isRecordingExpired(serializedRecording) ? null : serializedRecording;
|
|
8463
|
+
}.bind(this))
|
|
8464
|
+
.catch(this.handleError.bind(this));
|
|
8465
|
+
};
|
|
8466
|
+
|
|
8467
|
+
RecordingRegistry.prototype.clearActiveRecording = function () {
|
|
8468
|
+
// mark recording as expired instead of deleting it in case the page unloads mid-flush and doesn't make it to ingestion.
|
|
8469
|
+
// this will ensure the next pageload will flush the remaining events, but not try to continue the recording.
|
|
8470
|
+
return this.getActiveRecording()
|
|
8471
|
+
.then(function (serializedRecording) {
|
|
8472
|
+
if (serializedRecording) {
|
|
8473
|
+
serializedRecording['maxExpires'] = 0;
|
|
8474
|
+
return this.setActiveRecording(serializedRecording);
|
|
8475
|
+
}
|
|
8476
|
+
}.bind(this))
|
|
8477
|
+
.catch(this.handleError.bind(this));
|
|
8478
|
+
};
|
|
8479
|
+
|
|
8480
|
+
/**
|
|
8481
|
+
* Flush any inactive recordings from the registry to minimize data loss.
|
|
8482
|
+
* The main idea here is that we can flush remaining rrweb events on the next page load if a tab is closed mid-batch.
|
|
8483
|
+
*/
|
|
8484
|
+
RecordingRegistry.prototype.flushInactiveRecordings = function () {
|
|
8485
|
+
return this.idb.init()
|
|
8486
|
+
.then(function() {
|
|
8487
|
+
return this.idb.getAll();
|
|
8488
|
+
}.bind(this))
|
|
8489
|
+
.then(function (serializedRecordings) {
|
|
8490
|
+
// clean up any expired recordings from the registry, non-expired ones may be active in other tabs
|
|
8491
|
+
var unloadPromises = serializedRecordings
|
|
8492
|
+
.filter(function (serializedRecording) {
|
|
8493
|
+
return isRecordingExpired(serializedRecording);
|
|
8494
|
+
})
|
|
8495
|
+
.map(function (serializedRecording) {
|
|
8496
|
+
var sessionRecording = SessionRecording.deserialize(serializedRecording, {
|
|
8497
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8498
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8499
|
+
});
|
|
8500
|
+
return sessionRecording.unloadPersistedData()
|
|
8501
|
+
.then(function () {
|
|
8502
|
+
// expired recording was successfully flushed, we can clean it up from the registry
|
|
8503
|
+
return this.idb.removeItem(serializedRecording['tabId']);
|
|
8504
|
+
}.bind(this))
|
|
8505
|
+
.catch(this.handleError.bind(this));
|
|
8506
|
+
}.bind(this));
|
|
8507
|
+
|
|
8508
|
+
return PromisePolyfill.all(unloadPromises);
|
|
8509
|
+
}.bind(this))
|
|
8510
|
+
.catch(this.handleError.bind(this));
|
|
8511
|
+
};
|
|
8512
|
+
|
|
8129
8513
|
var logger$1 = console_with_prefix('recorder');
|
|
8130
8514
|
|
|
8131
8515
|
/**
|
|
8132
|
-
* Recorder API:
|
|
8516
|
+
* Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
|
|
8133
8517
|
* @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
8134
|
-
|
|
8135
|
-
var MixpanelRecorder = function(mixpanelInstance) {
|
|
8136
|
-
this.
|
|
8518
|
+
*/
|
|
8519
|
+
var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage) {
|
|
8520
|
+
this.mixpanelInstance = mixpanelInstance;
|
|
8521
|
+
this.rrwebRecord = rrwebRecord || record;
|
|
8522
|
+
this.sharedLockStorage = sharedLockStorage;
|
|
8523
|
+
|
|
8524
|
+
/**
|
|
8525
|
+
* @member {import('./registry').RecordingRegistry}
|
|
8526
|
+
*/
|
|
8527
|
+
this.recordingRegistry = new RecordingRegistry({
|
|
8528
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8529
|
+
errorReporter: logger$1.error,
|
|
8530
|
+
sharedLockStorage: sharedLockStorage
|
|
8531
|
+
});
|
|
8532
|
+
this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
|
|
8533
|
+
|
|
8137
8534
|
this.activeRecording = null;
|
|
8138
8535
|
};
|
|
8139
8536
|
|
|
8140
|
-
MixpanelRecorder.prototype.startRecording = function(
|
|
8537
|
+
MixpanelRecorder.prototype.startRecording = function(options) {
|
|
8538
|
+
options = options || {};
|
|
8141
8539
|
if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
|
|
8142
8540
|
logger$1.log('Recording already in progress, skipping startRecording.');
|
|
8143
8541
|
return;
|
|
8144
8542
|
}
|
|
8145
8543
|
|
|
8146
|
-
var onIdleTimeout =
|
|
8544
|
+
var onIdleTimeout = function () {
|
|
8147
8545
|
logger$1.log('Idle timeout reached, restarting recording.');
|
|
8148
8546
|
this.resetRecording();
|
|
8149
|
-
}
|
|
8547
|
+
}.bind(this);
|
|
8150
8548
|
|
|
8151
|
-
var onMaxLengthReached =
|
|
8549
|
+
var onMaxLengthReached = function () {
|
|
8152
8550
|
logger$1.log('Max recording length reached, stopping recording.');
|
|
8153
8551
|
this.resetRecording();
|
|
8154
|
-
}
|
|
8552
|
+
}.bind(this);
|
|
8553
|
+
|
|
8554
|
+
var onBatchSent = function () {
|
|
8555
|
+
this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8556
|
+
this['__flushPromise'] = this.activeRecording.batcher._flushPromise;
|
|
8557
|
+
}.bind(this);
|
|
8155
8558
|
|
|
8156
|
-
|
|
8157
|
-
|
|
8559
|
+
/**
|
|
8560
|
+
* @type {import('./session-recording').SessionRecordingOptions}
|
|
8561
|
+
*/
|
|
8562
|
+
var sessionRecordingOptions = {
|
|
8563
|
+
mixpanelInstance: this.mixpanelInstance,
|
|
8564
|
+
onBatchSent: onBatchSent,
|
|
8158
8565
|
onIdleTimeout: onIdleTimeout,
|
|
8159
8566
|
onMaxLengthReached: onMaxLengthReached,
|
|
8160
8567
|
replayId: _.UUID(),
|
|
8161
|
-
rrwebRecord:
|
|
8162
|
-
|
|
8568
|
+
rrwebRecord: this.rrwebRecord,
|
|
8569
|
+
sharedLockStorage: this.sharedLockStorage
|
|
8570
|
+
};
|
|
8163
8571
|
|
|
8164
|
-
|
|
8572
|
+
if (options.activeSerializedRecording) {
|
|
8573
|
+
this.activeRecording = SessionRecording.deserialize(options.activeSerializedRecording, sessionRecordingOptions);
|
|
8574
|
+
} else {
|
|
8575
|
+
this.activeRecording = new SessionRecording(sessionRecordingOptions);
|
|
8576
|
+
}
|
|
8577
|
+
|
|
8578
|
+
this.activeRecording.startRecording(options.shouldStopBatcher);
|
|
8579
|
+
return this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
|
|
8165
8580
|
};
|
|
8166
8581
|
|
|
8167
8582
|
MixpanelRecorder.prototype.stopRecording = function() {
|
|
8583
|
+
var stopPromise = this._stopCurrentRecording(false);
|
|
8584
|
+
this.recordingRegistry.clearActiveRecording();
|
|
8585
|
+
this.activeRecording = null;
|
|
8586
|
+
return stopPromise;
|
|
8587
|
+
};
|
|
8588
|
+
|
|
8589
|
+
MixpanelRecorder.prototype.pauseRecording = function() {
|
|
8590
|
+
return this._stopCurrentRecording(false);
|
|
8591
|
+
};
|
|
8592
|
+
|
|
8593
|
+
MixpanelRecorder.prototype._stopCurrentRecording = function(skipFlush) {
|
|
8168
8594
|
if (this.activeRecording) {
|
|
8169
|
-
this.activeRecording.stopRecording();
|
|
8170
|
-
|
|
8595
|
+
return this.activeRecording.stopRecording(skipFlush);
|
|
8596
|
+
}
|
|
8597
|
+
return PromisePolyfill.resolve();
|
|
8598
|
+
};
|
|
8599
|
+
|
|
8600
|
+
MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
|
|
8601
|
+
if (this.activeRecording && this.activeRecording.isRrwebStopped()) {
|
|
8602
|
+
this.activeRecording.startRecording(false);
|
|
8603
|
+
return PromisePolyfill.resolve(null);
|
|
8171
8604
|
}
|
|
8605
|
+
|
|
8606
|
+
return this.recordingRegistry.getActiveRecording()
|
|
8607
|
+
.then(function (activeSerializedRecording) {
|
|
8608
|
+
if (activeSerializedRecording) {
|
|
8609
|
+
return this.startRecording({activeSerializedRecording: activeSerializedRecording});
|
|
8610
|
+
} else if (startNewIfInactive) {
|
|
8611
|
+
return this.startRecording({shouldStopBatcher: false});
|
|
8612
|
+
} else {
|
|
8613
|
+
logger$1.log('No resumable recording found.');
|
|
8614
|
+
return null;
|
|
8615
|
+
}
|
|
8616
|
+
}.bind(this));
|
|
8172
8617
|
};
|
|
8173
8618
|
|
|
8619
|
+
|
|
8174
8620
|
MixpanelRecorder.prototype.resetRecording = function () {
|
|
8175
8621
|
this.stopRecording();
|
|
8176
|
-
this.startRecording(true);
|
|
8622
|
+
this.startRecording({shouldStopBatcher: true});
|
|
8177
8623
|
};
|
|
8178
8624
|
|
|
8179
8625
|
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
@@ -8262,7 +8708,7 @@ function getPreviousElementSibling(el) {
|
|
|
8262
8708
|
}
|
|
8263
8709
|
}
|
|
8264
8710
|
|
|
8265
|
-
function getPropertiesFromElement(el) {
|
|
8711
|
+
function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
|
|
8266
8712
|
var props = {
|
|
8267
8713
|
'$classes': getClassName(el).split(' '),
|
|
8268
8714
|
'$tag_name': el.tagName.toLowerCase()
|
|
@@ -8272,9 +8718,9 @@ function getPropertiesFromElement(el) {
|
|
|
8272
8718
|
props['$id'] = elId;
|
|
8273
8719
|
}
|
|
8274
8720
|
|
|
8275
|
-
if (
|
|
8276
|
-
_.each(TRACKED_ATTRS, function(attr) {
|
|
8277
|
-
if (el.hasAttribute(attr)) {
|
|
8721
|
+
if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
|
|
8722
|
+
_.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
|
|
8723
|
+
if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
|
|
8278
8724
|
var attrVal = el.getAttribute(attr);
|
|
8279
8725
|
if (shouldTrackValue(attrVal)) {
|
|
8280
8726
|
props['$attr-' + attr] = attrVal;
|
|
@@ -8298,8 +8744,21 @@ function getPropertiesFromElement(el) {
|
|
|
8298
8744
|
return props;
|
|
8299
8745
|
}
|
|
8300
8746
|
|
|
8301
|
-
function getPropsForDOMEvent(ev,
|
|
8302
|
-
|
|
8747
|
+
function getPropsForDOMEvent(ev, config) {
|
|
8748
|
+
var allowElementCallback = config.allowElementCallback;
|
|
8749
|
+
var allowSelectors = config.allowSelectors || [];
|
|
8750
|
+
var blockAttrs = config.blockAttrs || [];
|
|
8751
|
+
var blockElementCallback = config.blockElementCallback;
|
|
8752
|
+
var blockSelectors = config.blockSelectors || [];
|
|
8753
|
+
var captureTextContent = config.captureTextContent || false;
|
|
8754
|
+
var captureExtraAttrs = config.captureExtraAttrs || [];
|
|
8755
|
+
|
|
8756
|
+
// convert array to set every time, as the config may have changed
|
|
8757
|
+
var blockAttrsSet = {};
|
|
8758
|
+
_.each(blockAttrs, function(attr) {
|
|
8759
|
+
blockAttrsSet[attr] = true;
|
|
8760
|
+
});
|
|
8761
|
+
|
|
8303
8762
|
var props = null;
|
|
8304
8763
|
|
|
8305
8764
|
var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
|
|
@@ -8307,7 +8766,11 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
8307
8766
|
target = target.parentNode;
|
|
8308
8767
|
}
|
|
8309
8768
|
|
|
8310
|
-
if (
|
|
8769
|
+
if (
|
|
8770
|
+
shouldTrackDomEvent(target, ev) &&
|
|
8771
|
+
isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
|
|
8772
|
+
!isElementBlocked(target, ev, blockElementCallback, blockSelectors)
|
|
8773
|
+
) {
|
|
8311
8774
|
var targetElementList = [target];
|
|
8312
8775
|
var curEl = target;
|
|
8313
8776
|
while (curEl.parentNode && !isTag(curEl, 'body')) {
|
|
@@ -8318,37 +8781,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
8318
8781
|
var elementsJson = [];
|
|
8319
8782
|
var href, explicitNoTrack = false;
|
|
8320
8783
|
_.each(targetElementList, function(el) {
|
|
8321
|
-
var
|
|
8784
|
+
var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
|
|
8322
8785
|
|
|
8323
8786
|
// if the element or a parent element is an anchor tag
|
|
8324
8787
|
// include the href as a property
|
|
8325
|
-
if (el.tagName.toLowerCase() === 'a') {
|
|
8788
|
+
if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
|
|
8326
8789
|
href = el.getAttribute('href');
|
|
8327
|
-
href =
|
|
8790
|
+
href = shouldTrackDetails && shouldTrackValue(href) && href;
|
|
8328
8791
|
}
|
|
8329
8792
|
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
_.each(OPT_OUT_CLASSES, function(cls) {
|
|
8333
|
-
if (classes[cls]) {
|
|
8334
|
-
explicitNoTrack = true;
|
|
8335
|
-
}
|
|
8336
|
-
});
|
|
8337
|
-
|
|
8338
|
-
if (!explicitNoTrack) {
|
|
8339
|
-
// programmatically prevent tracking of elements that match CSS selectors
|
|
8340
|
-
_.each(blockSelectors, function(sel) {
|
|
8341
|
-
try {
|
|
8342
|
-
if (el['matches'](sel)) {
|
|
8343
|
-
explicitNoTrack = true;
|
|
8344
|
-
}
|
|
8345
|
-
} catch (err) {
|
|
8346
|
-
logger.critical('Error while checking selector: ' + sel, err);
|
|
8347
|
-
}
|
|
8348
|
-
});
|
|
8793
|
+
if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
|
|
8794
|
+
explicitNoTrack = true;
|
|
8349
8795
|
}
|
|
8350
8796
|
|
|
8351
|
-
elementsJson.push(getPropertiesFromElement(el));
|
|
8797
|
+
elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
|
|
8352
8798
|
}, this);
|
|
8353
8799
|
|
|
8354
8800
|
if (!explicitNoTrack) {
|
|
@@ -8362,9 +8808,17 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
8362
8808
|
'$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
|
|
8363
8809
|
'$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
|
|
8364
8810
|
};
|
|
8811
|
+
_.each(captureExtraAttrs, function(attr) {
|
|
8812
|
+
if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
|
|
8813
|
+
var attrVal = target.getAttribute(attr);
|
|
8814
|
+
if (shouldTrackValue(attrVal)) {
|
|
8815
|
+
props['$el_attr__' + attr] = attrVal;
|
|
8816
|
+
}
|
|
8817
|
+
}
|
|
8818
|
+
});
|
|
8365
8819
|
|
|
8366
8820
|
if (captureTextContent) {
|
|
8367
|
-
elementText = getSafeText(target);
|
|
8821
|
+
elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
|
|
8368
8822
|
if (elementText && elementText.length) {
|
|
8369
8823
|
props['$el_text'] = elementText;
|
|
8370
8824
|
}
|
|
@@ -8380,14 +8834,22 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
8380
8834
|
}
|
|
8381
8835
|
// prioritize text content from "real" click target if different from original target
|
|
8382
8836
|
if (captureTextContent) {
|
|
8383
|
-
var elementText = getSafeText(target);
|
|
8837
|
+
var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
|
|
8384
8838
|
if (elementText && elementText.length) {
|
|
8385
8839
|
props['$el_text'] = elementText;
|
|
8386
8840
|
}
|
|
8387
8841
|
}
|
|
8388
8842
|
|
|
8389
8843
|
if (target) {
|
|
8390
|
-
|
|
8844
|
+
// target may have been recalculated; check allowlists and blocklists again
|
|
8845
|
+
if (
|
|
8846
|
+
!isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
|
|
8847
|
+
isElementBlocked(target, ev, blockElementCallback, blockSelectors)
|
|
8848
|
+
) {
|
|
8849
|
+
return null;
|
|
8850
|
+
}
|
|
8851
|
+
|
|
8852
|
+
var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
|
|
8391
8853
|
props['$target'] = targetProps;
|
|
8392
8854
|
// pull up more props onto main event props
|
|
8393
8855
|
props['$el_classes'] = targetProps['$classes'];
|
|
@@ -8403,19 +8865,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
8403
8865
|
}
|
|
8404
8866
|
|
|
8405
8867
|
|
|
8406
|
-
|
|
8868
|
+
/**
|
|
8407
8869
|
* Get the direct text content of an element, protecting against sensitive data collection.
|
|
8408
8870
|
* Concats textContent of each of the element's text node children; this avoids potential
|
|
8409
8871
|
* collection of sensitive data that could happen if we used element.textContent and the
|
|
8410
8872
|
* element had sensitive child elements, since element.textContent includes child content.
|
|
8411
8873
|
* Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
|
|
8412
8874
|
* @param {Element} el - element to get the text of
|
|
8875
|
+
* @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
|
|
8413
8876
|
* @returns {string} the element's direct text content
|
|
8414
8877
|
*/
|
|
8415
|
-
function getSafeText(el) {
|
|
8878
|
+
function getSafeText(el, ev, allowElementCallback, allowSelectors) {
|
|
8416
8879
|
var elText = '';
|
|
8417
8880
|
|
|
8418
|
-
if (
|
|
8881
|
+
if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
|
|
8419
8882
|
_.each(el.childNodes, function(child) {
|
|
8420
8883
|
if (isTextNode(child) && child.textContent) {
|
|
8421
8884
|
elText += _.trim(child.textContent)
|
|
@@ -8454,6 +8917,75 @@ function guessRealClickTarget(ev) {
|
|
|
8454
8917
|
return target;
|
|
8455
8918
|
}
|
|
8456
8919
|
|
|
8920
|
+
function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
|
|
8921
|
+
if (allowElementCallback) {
|
|
8922
|
+
try {
|
|
8923
|
+
if (!allowElementCallback(el, ev)) {
|
|
8924
|
+
return false;
|
|
8925
|
+
}
|
|
8926
|
+
} catch (err) {
|
|
8927
|
+
logger.critical('Error while checking element in allowElementCallback', err);
|
|
8928
|
+
return false;
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
|
|
8932
|
+
if (!allowSelectors.length) {
|
|
8933
|
+
// no allowlist; all elements are fair game
|
|
8934
|
+
return true;
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
for (var i = 0; i < allowSelectors.length; i++) {
|
|
8938
|
+
var sel = allowSelectors[i];
|
|
8939
|
+
try {
|
|
8940
|
+
if (el['matches'](sel)) {
|
|
8941
|
+
return true;
|
|
8942
|
+
}
|
|
8943
|
+
} catch (err) {
|
|
8944
|
+
logger.critical('Error while checking selector: ' + sel, err);
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
return false;
|
|
8948
|
+
}
|
|
8949
|
+
|
|
8950
|
+
function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
|
|
8951
|
+
var i;
|
|
8952
|
+
|
|
8953
|
+
if (blockElementCallback) {
|
|
8954
|
+
try {
|
|
8955
|
+
if (blockElementCallback(el, ev)) {
|
|
8956
|
+
return true;
|
|
8957
|
+
}
|
|
8958
|
+
} catch (err) {
|
|
8959
|
+
logger.critical('Error while checking element in blockElementCallback', err);
|
|
8960
|
+
return true;
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
|
|
8964
|
+
if (blockSelectors && blockSelectors.length) {
|
|
8965
|
+
// programmatically prevent tracking of elements that match CSS selectors
|
|
8966
|
+
for (i = 0; i < blockSelectors.length; i++) {
|
|
8967
|
+
var sel = blockSelectors[i];
|
|
8968
|
+
try {
|
|
8969
|
+
if (el['matches'](sel)) {
|
|
8970
|
+
return true;
|
|
8971
|
+
}
|
|
8972
|
+
} catch (err) {
|
|
8973
|
+
logger.critical('Error while checking selector: ' + sel, err);
|
|
8974
|
+
}
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
|
|
8978
|
+
// allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
|
|
8979
|
+
var classes = getClasses(el);
|
|
8980
|
+
for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
|
|
8981
|
+
if (classes[OPT_OUT_CLASSES[i]]) {
|
|
8982
|
+
return true;
|
|
8983
|
+
}
|
|
8984
|
+
}
|
|
8985
|
+
|
|
8986
|
+
return false;
|
|
8987
|
+
}
|
|
8988
|
+
|
|
8457
8989
|
/*
|
|
8458
8990
|
* Check whether a DOM node has nodeType Node.ELEMENT_NODE
|
|
8459
8991
|
* @param {Node} node - node to check
|
|
@@ -8528,11 +9060,16 @@ function shouldTrackDomEvent(el, ev) {
|
|
|
8528
9060
|
* Check whether a DOM element should be "tracked" or if it may contain sensitive data
|
|
8529
9061
|
* using a variety of heuristics.
|
|
8530
9062
|
* @param {Element} el - element to check
|
|
9063
|
+
* @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
|
|
8531
9064
|
* @returns {boolean} whether the element should be tracked
|
|
8532
9065
|
*/
|
|
8533
|
-
function
|
|
9066
|
+
function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
|
|
8534
9067
|
var i;
|
|
8535
9068
|
|
|
9069
|
+
if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
|
|
9070
|
+
return false;
|
|
9071
|
+
}
|
|
9072
|
+
|
|
8536
9073
|
for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
|
|
8537
9074
|
var classes = getClasses(curEl);
|
|
8538
9075
|
for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
|
|
@@ -8622,9 +9159,17 @@ var PAGEVIEW_OPTION_FULL_URL = 'full-url';
|
|
|
8622
9159
|
var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
|
|
8623
9160
|
var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
|
|
8624
9161
|
|
|
9162
|
+
var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
|
|
9163
|
+
var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
|
|
9164
|
+
var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
|
|
9165
|
+
var CONFIG_BLOCK_ATTRS = 'block_attrs';
|
|
9166
|
+
var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
|
|
8625
9167
|
var CONFIG_BLOCK_SELECTORS = 'block_selectors';
|
|
8626
9168
|
var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
|
|
9169
|
+
var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
|
|
8627
9170
|
var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
|
|
9171
|
+
var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
|
|
9172
|
+
var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
|
|
8628
9173
|
var CONFIG_TRACK_CLICK = 'click';
|
|
8629
9174
|
var CONFIG_TRACK_INPUT = 'input';
|
|
8630
9175
|
var CONFIG_TRACK_PAGEVIEW = 'pageview';
|
|
@@ -8632,7 +9177,16 @@ var CONFIG_TRACK_SCROLL = 'scroll';
|
|
|
8632
9177
|
var CONFIG_TRACK_SUBMIT = 'submit';
|
|
8633
9178
|
|
|
8634
9179
|
var CONFIG_DEFAULTS = {};
|
|
9180
|
+
CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
|
|
9181
|
+
CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
|
|
9182
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
|
|
9183
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
|
|
9184
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
|
|
9185
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
|
|
9186
|
+
CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
|
|
8635
9187
|
CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
|
|
9188
|
+
CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
|
|
9189
|
+
CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
|
|
8636
9190
|
CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
|
|
8637
9191
|
CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
|
|
8638
9192
|
CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
|
|
@@ -8687,13 +9241,37 @@ Autocapture.prototype.getConfig = function(key) {
|
|
|
8687
9241
|
};
|
|
8688
9242
|
|
|
8689
9243
|
Autocapture.prototype.currentUrlBlocked = function() {
|
|
9244
|
+
var i;
|
|
9245
|
+
var currentUrl = _.info.currentUrl();
|
|
9246
|
+
|
|
9247
|
+
var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
|
|
9248
|
+
if (allowUrlRegexes.length) {
|
|
9249
|
+
// we're using an allowlist, only track if current URL matches
|
|
9250
|
+
var allowed = false;
|
|
9251
|
+
for (i = 0; i < allowUrlRegexes.length; i++) {
|
|
9252
|
+
var allowRegex = allowUrlRegexes[i];
|
|
9253
|
+
try {
|
|
9254
|
+
if (currentUrl.match(allowRegex)) {
|
|
9255
|
+
allowed = true;
|
|
9256
|
+
break;
|
|
9257
|
+
}
|
|
9258
|
+
} catch (err) {
|
|
9259
|
+
logger.critical('Error while checking block URL regex: ' + allowRegex, err);
|
|
9260
|
+
return true;
|
|
9261
|
+
}
|
|
9262
|
+
}
|
|
9263
|
+
if (!allowed) {
|
|
9264
|
+
// wasn't allowed by any regex
|
|
9265
|
+
return true;
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
|
|
8690
9269
|
var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
|
|
8691
9270
|
if (!blockUrlRegexes || !blockUrlRegexes.length) {
|
|
8692
9271
|
return false;
|
|
8693
9272
|
}
|
|
8694
9273
|
|
|
8695
|
-
|
|
8696
|
-
for (var i = 0; i < blockUrlRegexes.length; i++) {
|
|
9274
|
+
for (i = 0; i < blockUrlRegexes.length; i++) {
|
|
8697
9275
|
try {
|
|
8698
9276
|
if (currentUrl.match(blockUrlRegexes[i])) {
|
|
8699
9277
|
return true;
|
|
@@ -8721,11 +9299,15 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
|
|
|
8721
9299
|
return;
|
|
8722
9300
|
}
|
|
8723
9301
|
|
|
8724
|
-
var props = getPropsForDOMEvent(
|
|
8725
|
-
|
|
8726
|
-
this.getConfig(
|
|
8727
|
-
this.getConfig(
|
|
8728
|
-
|
|
9302
|
+
var props = getPropsForDOMEvent(ev, {
|
|
9303
|
+
allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
|
|
9304
|
+
allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
|
|
9305
|
+
blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
|
|
9306
|
+
blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
|
|
9307
|
+
blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
|
|
9308
|
+
captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
|
|
9309
|
+
captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
|
|
9310
|
+
});
|
|
8729
9311
|
if (props) {
|
|
8730
9312
|
_.extend(props, DEFAULT_PROPS);
|
|
8731
9313
|
this.mp.track(mpEventName, props);
|
|
@@ -8810,13 +9392,14 @@ Autocapture.prototype.initPageviewTracking = function() {
|
|
|
8810
9392
|
|
|
8811
9393
|
var currentUrl = _.info.currentUrl();
|
|
8812
9394
|
var shouldTrack = false;
|
|
9395
|
+
var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
|
|
8813
9396
|
var trackPageviewOption = this.pageviewTrackingConfig();
|
|
8814
9397
|
if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
|
|
8815
9398
|
shouldTrack = currentUrl !== previousTrackedUrl;
|
|
8816
9399
|
} else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
|
|
8817
9400
|
shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
|
|
8818
9401
|
} else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
|
|
8819
|
-
shouldTrack =
|
|
9402
|
+
shouldTrack = didPathChange;
|
|
8820
9403
|
}
|
|
8821
9404
|
|
|
8822
9405
|
if (shouldTrack) {
|
|
@@ -8824,6 +9407,10 @@ Autocapture.prototype.initPageviewTracking = function() {
|
|
|
8824
9407
|
if (tracked) {
|
|
8825
9408
|
previousTrackedUrl = currentUrl;
|
|
8826
9409
|
}
|
|
9410
|
+
if (didPathChange) {
|
|
9411
|
+
this.lastScrollCheckpoint = 0;
|
|
9412
|
+
logger.log('Path change: re-initializing scroll depth checkpoints');
|
|
9413
|
+
}
|
|
8827
9414
|
}
|
|
8828
9415
|
}.bind(this)));
|
|
8829
9416
|
};
|
|
@@ -8835,6 +9422,7 @@ Autocapture.prototype.initScrollTracking = function() {
|
|
|
8835
9422
|
return;
|
|
8836
9423
|
}
|
|
8837
9424
|
logger.log('Initializing scroll tracking');
|
|
9425
|
+
this.lastScrollCheckpoint = 0;
|
|
8838
9426
|
|
|
8839
9427
|
this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
|
|
8840
9428
|
if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
|
|
@@ -8844,6 +9432,11 @@ Autocapture.prototype.initScrollTracking = function() {
|
|
|
8844
9432
|
return;
|
|
8845
9433
|
}
|
|
8846
9434
|
|
|
9435
|
+
var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
|
|
9436
|
+
var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
|
|
9437
|
+
.slice()
|
|
9438
|
+
.sort(function(a, b) { return a - b; });
|
|
9439
|
+
|
|
8847
9440
|
var scrollTop = win.scrollY;
|
|
8848
9441
|
var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
|
|
8849
9442
|
try {
|
|
@@ -8851,10 +9444,25 @@ Autocapture.prototype.initScrollTracking = function() {
|
|
|
8851
9444
|
var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
|
|
8852
9445
|
props['$scroll_height'] = scrollHeight;
|
|
8853
9446
|
props['$scroll_percentage'] = scrollPercentage;
|
|
9447
|
+
if (scrollPercentage > this.lastScrollCheckpoint) {
|
|
9448
|
+
for (var i = 0; i < scrollCheckpoints.length; i++) {
|
|
9449
|
+
var checkpoint = scrollCheckpoints[i];
|
|
9450
|
+
if (
|
|
9451
|
+
scrollPercentage >= checkpoint &&
|
|
9452
|
+
this.lastScrollCheckpoint < checkpoint
|
|
9453
|
+
) {
|
|
9454
|
+
props['$scroll_checkpoint'] = checkpoint;
|
|
9455
|
+
this.lastScrollCheckpoint = checkpoint;
|
|
9456
|
+
shouldTrack = true;
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
}
|
|
8854
9460
|
} catch (err) {
|
|
8855
9461
|
logger.critical('Error while calculating scroll percentage', err);
|
|
8856
9462
|
}
|
|
8857
|
-
|
|
9463
|
+
if (shouldTrack) {
|
|
9464
|
+
this.mp.track(MP_EV_SCROLL, props);
|
|
9465
|
+
}
|
|
8858
9466
|
}.bind(this)));
|
|
8859
9467
|
};
|
|
8860
9468
|
|
|
@@ -10123,8 +10731,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
|
|
|
10123
10731
|
if (!(k in union_q)) {
|
|
10124
10732
|
union_q[k] = [];
|
|
10125
10733
|
}
|
|
10126
|
-
//
|
|
10127
|
-
|
|
10734
|
+
// Prevent duplicate values
|
|
10735
|
+
_.each(v, function(item) {
|
|
10736
|
+
if (!_.include(union_q[k], item)) {
|
|
10737
|
+
union_q[k].push(item);
|
|
10738
|
+
}
|
|
10739
|
+
});
|
|
10128
10740
|
}
|
|
10129
10741
|
});
|
|
10130
10742
|
this._pop_from_people_queue(UNSET_ACTION, q_data);
|
|
@@ -10223,11 +10835,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
|
|
|
10223
10835
|
* Released under the MIT License.
|
|
10224
10836
|
*/
|
|
10225
10837
|
|
|
10226
|
-
// ==ClosureCompiler==
|
|
10227
|
-
// @compilation_level ADVANCED_OPTIMIZATIONS
|
|
10228
|
-
// @output_file_name mixpanel-2.8.min.js
|
|
10229
|
-
// ==/ClosureCompiler==
|
|
10230
|
-
|
|
10231
10838
|
/*
|
|
10232
10839
|
SIMPLE STYLE GUIDE:
|
|
10233
10840
|
|
|
@@ -10250,7 +10857,6 @@ var INIT_MODULE = 0;
|
|
|
10250
10857
|
var INIT_SNIPPET = 1;
|
|
10251
10858
|
|
|
10252
10859
|
var IDENTITY_FUNC = function(x) {return x;};
|
|
10253
|
-
var NOOP_FUNC = function() {};
|
|
10254
10860
|
|
|
10255
10861
|
/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
|
|
10256
10862
|
/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
|
|
@@ -10559,34 +11165,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
10559
11165
|
this.autocapture = new Autocapture(this);
|
|
10560
11166
|
this.autocapture.init();
|
|
10561
11167
|
|
|
10562
|
-
|
|
10563
|
-
|
|
11168
|
+
this._init_tab_id();
|
|
11169
|
+
this._check_and_start_session_recording();
|
|
11170
|
+
};
|
|
11171
|
+
|
|
11172
|
+
/**
|
|
11173
|
+
* Assigns a unique UUID to this tab / window by leveraging sessionStorage.
|
|
11174
|
+
* This is primarily used for session recording, where data must be isolated to the current tab.
|
|
11175
|
+
*/
|
|
11176
|
+
MixpanelLib.prototype._init_tab_id = function() {
|
|
11177
|
+
if (_.sessionStorage.is_supported()) {
|
|
11178
|
+
try {
|
|
11179
|
+
var key_suffix = this.get_config('name') + '_' + this.get_config('token');
|
|
11180
|
+
var tab_id_key = 'mp_tab_id_' + key_suffix;
|
|
11181
|
+
|
|
11182
|
+
// A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
|
|
11183
|
+
// This enforces a unique ID in the cases like duplicated tab, window.open(...)
|
|
11184
|
+
var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
|
|
11185
|
+
if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
|
|
11186
|
+
_.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
|
|
11187
|
+
}
|
|
11188
|
+
|
|
11189
|
+
_.sessionStorage.set(should_generate_new_tab_id_key, '1');
|
|
11190
|
+
this.tab_id = _.sessionStorage.get(tab_id_key);
|
|
11191
|
+
|
|
11192
|
+
// Remove the flag when the tab is unloaded to indicate the stored tab ID can be reused. This event is not reliable to detect all page unloads,
|
|
11193
|
+
// but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
|
|
11194
|
+
// If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
|
|
11195
|
+
win.addEventListener('beforeunload', function () {
|
|
11196
|
+
_.sessionStorage.remove(should_generate_new_tab_id_key);
|
|
11197
|
+
});
|
|
11198
|
+
} catch(err) {
|
|
11199
|
+
this.report_error('Error initializing tab id', err);
|
|
11200
|
+
}
|
|
11201
|
+
} else {
|
|
11202
|
+
this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
|
|
10564
11203
|
}
|
|
10565
11204
|
};
|
|
10566
11205
|
|
|
10567
|
-
MixpanelLib.prototype.
|
|
11206
|
+
MixpanelLib.prototype.get_tab_id = function () {
|
|
11207
|
+
return this.tab_id || null;
|
|
11208
|
+
};
|
|
11209
|
+
|
|
11210
|
+
MixpanelLib.prototype._should_load_recorder = function () {
|
|
11211
|
+
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
11212
|
+
var tab_id = this.get_tab_id();
|
|
11213
|
+
return recording_registry_idb.init()
|
|
11214
|
+
.then(function () {
|
|
11215
|
+
return recording_registry_idb.getAll();
|
|
11216
|
+
})
|
|
11217
|
+
.then(function (recordings) {
|
|
11218
|
+
for (var i = 0; i < recordings.length; i++) {
|
|
11219
|
+
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
11220
|
+
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
11221
|
+
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
11222
|
+
return true;
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
return false;
|
|
11226
|
+
})
|
|
11227
|
+
.catch(_.bind(function (err) {
|
|
11228
|
+
this.report_error('Error checking recording registry', err);
|
|
11229
|
+
}, this));
|
|
11230
|
+
};
|
|
11231
|
+
|
|
11232
|
+
MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
|
|
10568
11233
|
if (!win['MutationObserver']) {
|
|
10569
11234
|
console$1.critical('Browser does not support MutationObserver; skipping session recording');
|
|
10570
11235
|
return;
|
|
10571
11236
|
}
|
|
10572
11237
|
|
|
10573
|
-
var
|
|
10574
|
-
|
|
10575
|
-
|
|
11238
|
+
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
11239
|
+
var handleLoadedRecorder = _.bind(function() {
|
|
11240
|
+
this._recorder = this._recorder || new win['__mp_recorder'](this);
|
|
11241
|
+
this._recorder['resumeRecording'](startNewIfInactive);
|
|
11242
|
+
}, this);
|
|
11243
|
+
|
|
11244
|
+
if (_.isUndefined(win['__mp_recorder'])) {
|
|
11245
|
+
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
11246
|
+
} else {
|
|
11247
|
+
handleLoadedRecorder();
|
|
11248
|
+
}
|
|
10576
11249
|
}, this);
|
|
10577
11250
|
|
|
10578
|
-
|
|
10579
|
-
|
|
11251
|
+
/**
|
|
11252
|
+
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
11253
|
+
* Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
|
|
11254
|
+
*/
|
|
11255
|
+
var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
|
|
11256
|
+
if (force_start || is_sampled) {
|
|
11257
|
+
loadRecorder(true);
|
|
10580
11258
|
} else {
|
|
10581
|
-
|
|
11259
|
+
this._should_load_recorder()
|
|
11260
|
+
.then(function (shouldLoad) {
|
|
11261
|
+
if (shouldLoad) {
|
|
11262
|
+
loadRecorder(false);
|
|
11263
|
+
}
|
|
11264
|
+
});
|
|
10582
11265
|
}
|
|
10583
11266
|
});
|
|
10584
11267
|
|
|
11268
|
+
MixpanelLib.prototype.start_session_recording = function () {
|
|
11269
|
+
this._check_and_start_session_recording(true);
|
|
11270
|
+
};
|
|
11271
|
+
|
|
10585
11272
|
MixpanelLib.prototype.stop_session_recording = function () {
|
|
10586
11273
|
if (this._recorder) {
|
|
10587
11274
|
this._recorder['stopRecording']();
|
|
10588
|
-
}
|
|
10589
|
-
|
|
11275
|
+
}
|
|
11276
|
+
};
|
|
11277
|
+
|
|
11278
|
+
MixpanelLib.prototype.pause_session_recording = function () {
|
|
11279
|
+
if (this._recorder) {
|
|
11280
|
+
this._recorder['pauseRecording']();
|
|
11281
|
+
}
|
|
11282
|
+
};
|
|
11283
|
+
|
|
11284
|
+
MixpanelLib.prototype.resume_session_recording = function () {
|
|
11285
|
+
if (this._recorder) {
|
|
11286
|
+
this._recorder['resumeRecording']();
|
|
10590
11287
|
}
|
|
10591
11288
|
};
|
|
10592
11289
|
|
|
@@ -10621,6 +11318,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
|
|
|
10621
11318
|
return replay_id || null;
|
|
10622
11319
|
};
|
|
10623
11320
|
|
|
11321
|
+
// "private" public method to reach into the recorder in test cases
|
|
11322
|
+
MixpanelLib.prototype.__get_recorder = function () {
|
|
11323
|
+
return this._recorder;
|
|
11324
|
+
};
|
|
11325
|
+
|
|
10624
11326
|
// Private methods
|
|
10625
11327
|
|
|
10626
11328
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -10960,7 +11662,8 @@ MixpanelLib.prototype.init_batchers = function() {
|
|
|
10960
11662
|
return this._run_hook('before_send_' + attrs.type, item);
|
|
10961
11663
|
}, this),
|
|
10962
11664
|
stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
|
|
10963
|
-
usePersistence: true
|
|
11665
|
+
usePersistence: true,
|
|
11666
|
+
enqueueThrottleMs: 10,
|
|
10964
11667
|
}
|
|
10965
11668
|
);
|
|
10966
11669
|
}, this);
|
|
@@ -12061,6 +12764,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
|
|
|
12061
12764
|
|
|
12062
12765
|
if (disabled) {
|
|
12063
12766
|
this.stop_batch_senders();
|
|
12767
|
+
this.stop_session_recording();
|
|
12064
12768
|
} else {
|
|
12065
12769
|
// only start batchers after opt-in if they have previously been started
|
|
12066
12770
|
// in order to avoid unintentionally starting up batching for the first time
|
|
@@ -12301,10 +13005,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
|
|
|
12301
13005
|
MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
|
|
12302
13006
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
12303
13007
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
13008
|
+
MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
|
|
13009
|
+
MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
|
|
12304
13010
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
12305
13011
|
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
13012
|
+
MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
|
|
12306
13013
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
12307
13014
|
|
|
13015
|
+
// Exports intended only for testing
|
|
13016
|
+
MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
|
|
13017
|
+
|
|
12308
13018
|
// MixpanelPersistence Exports
|
|
12309
13019
|
MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
|
|
12310
13020
|
MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;
|