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