mixpanel-browser 2.74.0 → 2.76.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/.claude/settings.local.json +3 -1
- package/.github/workflows/integration-tests.yml +2 -2
- package/.github/workflows/unit-tests.yml +3 -3
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/build.sh +10 -8
- package/dist/async-modules/mixpanel-recorder-bIS4LMGd.js +23595 -0
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +2 -0
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +1 -0
- package/dist/async-modules/mixpanel-targeting-BcAPS-Mz.js +2520 -0
- package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js +2 -0
- package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js.map +1 -0
- package/dist/mixpanel-core.cjs.d.ts +68 -0
- package/dist/mixpanel-core.cjs.js +802 -337
- package/dist/mixpanel-recorder.js +828 -40
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +2520 -0
- package/dist/mixpanel-targeting.min.js +2 -0
- package/dist/mixpanel-targeting.min.js.map +1 -0
- package/dist/mixpanel-with-async-modules.cjs.d.ts +590 -0
- package/dist/mixpanel-with-async-modules.cjs.js +9867 -0
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +802 -337
- package/dist/mixpanel-with-recorder.d.ts +68 -0
- package/dist/mixpanel-with-recorder.js +1591 -343
- package/dist/mixpanel-with-recorder.min.d.ts +68 -0
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +68 -0
- package/dist/mixpanel.amd.js +2124 -345
- package/dist/mixpanel.cjs.d.ts +68 -0
- package/dist/mixpanel.cjs.js +2124 -345
- package/dist/mixpanel.globals.js +802 -337
- package/dist/mixpanel.min.js +185 -175
- package/dist/mixpanel.module.d.ts +68 -0
- package/dist/mixpanel.module.js +2124 -345
- package/dist/mixpanel.umd.d.ts +68 -0
- package/dist/mixpanel.umd.js +2124 -345
- package/dist/rrweb-bundled.js +119 -5
- package/dist/rrweb-compiled.js +116 -5
- package/logo.svg +5 -0
- package/package.json +5 -3
- package/rollup.config.mjs +189 -40
- package/src/autocapture/index.js +10 -27
- package/src/config.js +9 -3
- package/src/flags/index.js +269 -9
- package/src/index.d.ts +68 -0
- package/src/loaders/loader-module.js +1 -0
- package/src/mixpanel-core.js +83 -109
- package/src/recorder/index.js +2 -1
- package/src/recorder/recorder.js +5 -1
- package/src/recorder/rrweb-network-plugin.js +649 -0
- package/src/recorder/session-recording.js +31 -11
- package/src/recorder-manager.js +216 -0
- package/src/request-batcher.js +1 -1
- package/src/targeting/event-matcher.js +42 -0
- package/src/targeting/index.js +11 -0
- package/src/targeting/loader.js +36 -0
- package/src/utils.js +14 -9
- package/testServer.js +55 -0
- /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
|
@@ -26,6 +26,19 @@
|
|
|
26
26
|
win = window;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
var Config = {
|
|
30
|
+
DEBUG: false,
|
|
31
|
+
LIB_VERSION: '2.76.0'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Window global names for async modules
|
|
35
|
+
var TARGETING_GLOBAL_NAME = '__mp_targeting';
|
|
36
|
+
var RECORDER_GLOBAL_NAME = '__mp_recorder';
|
|
37
|
+
|
|
38
|
+
// Constants that are injected at build-time for the names of async modules.
|
|
39
|
+
var RECORDER_FILENAME = '__MP_RECORDER_FILENAME__';
|
|
40
|
+
var TARGETING_FILENAME = '__MP_TARGETING_FILENAME__';
|
|
41
|
+
|
|
29
42
|
function _array_like_to_array(arr, len) {
|
|
30
43
|
if (len == null || len > arr.length) len = arr.length;
|
|
31
44
|
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
|
|
@@ -640,14 +653,16 @@
|
|
|
640
653
|
return this.nodeMetaMap.get(n2) || null;
|
|
641
654
|
};
|
|
642
655
|
// removes the node from idNodeMap
|
|
643
|
-
//
|
|
644
|
-
_proto.removeNodeFromMap = function removeNodeFromMap(n2) {
|
|
656
|
+
// if permanent is true, also removes from nodeMetaMap
|
|
657
|
+
_proto.removeNodeFromMap = function removeNodeFromMap(n2, permanent) {
|
|
645
658
|
var _this = this;
|
|
659
|
+
if (permanent === void 0) permanent = false;
|
|
646
660
|
var id = this.getId(n2);
|
|
647
661
|
this.idNodeMap.delete(id);
|
|
662
|
+
if (permanent) this.nodeMetaMap.delete(n2);
|
|
648
663
|
if (n2.childNodes) {
|
|
649
664
|
n2.childNodes.forEach(function(childNode) {
|
|
650
|
-
return _this.removeNodeFromMap(childNode);
|
|
665
|
+
return _this.removeNodeFromMap(childNode, permanent);
|
|
651
666
|
});
|
|
652
667
|
}
|
|
653
668
|
};
|
|
@@ -10389,6 +10404,15 @@
|
|
|
10389
10404
|
_proto.generateId = function generateId() {
|
|
10390
10405
|
return this.id++;
|
|
10391
10406
|
};
|
|
10407
|
+
_proto.remove = function remove(stylesheet) {
|
|
10408
|
+
var id = this.styleIDMap.get(stylesheet);
|
|
10409
|
+
if (id !== void 0) {
|
|
10410
|
+
this.styleIDMap.delete(stylesheet);
|
|
10411
|
+
this.idStyleMap.delete(id);
|
|
10412
|
+
return true;
|
|
10413
|
+
}
|
|
10414
|
+
return false;
|
|
10415
|
+
};
|
|
10392
10416
|
return StyleSheetMirror;
|
|
10393
10417
|
}();
|
|
10394
10418
|
function getShadowHost(n2) {
|
|
@@ -10711,7 +10735,15 @@
|
|
|
10711
10735
|
}
|
|
10712
10736
|
};
|
|
10713
10737
|
while(_this.mapRemoves.length){
|
|
10714
|
-
_this.
|
|
10738
|
+
var removedNode = _this.mapRemoves.shift();
|
|
10739
|
+
if (removedNode.nodeName === "IFRAME") {
|
|
10740
|
+
try {
|
|
10741
|
+
_this.iframeManager.removeIframe(removedNode);
|
|
10742
|
+
} catch (e2) {}
|
|
10743
|
+
} else {
|
|
10744
|
+
_this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
|
|
10745
|
+
}
|
|
10746
|
+
_this.mirror.removeNodeFromMap(removedNode);
|
|
10715
10747
|
}
|
|
10716
10748
|
for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
|
|
10717
10749
|
var n2 = _step.value;
|
|
@@ -11085,6 +11117,9 @@
|
|
|
11085
11117
|
this.shadowDomManager.reset();
|
|
11086
11118
|
this.canvasManager.reset();
|
|
11087
11119
|
};
|
|
11120
|
+
_proto.getDoc = function getDoc() {
|
|
11121
|
+
return this.doc;
|
|
11122
|
+
};
|
|
11088
11123
|
return MutationBuffer;
|
|
11089
11124
|
}();
|
|
11090
11125
|
function deepDelete(addsSet, n2) {
|
|
@@ -11185,6 +11220,14 @@
|
|
|
11185
11220
|
});
|
|
11186
11221
|
return observer;
|
|
11187
11222
|
}
|
|
11223
|
+
function removeMutationBufferForDoc(doc) {
|
|
11224
|
+
for(var i2 = mutationBuffers.length - 1; i2 >= 0; i2--){
|
|
11225
|
+
var buffer = mutationBuffers[i2];
|
|
11226
|
+
if (buffer.getDoc() === doc) {
|
|
11227
|
+
mutationBuffers.splice(i2, 1);
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
}
|
|
11188
11231
|
function initMoveObserver(param) {
|
|
11189
11232
|
var mousemoveCb = param.mousemoveCb, sampling = param.sampling, doc = param.doc, mirror2 = param.mirror;
|
|
11190
11233
|
if (sampling.mousemove === false) {
|
|
@@ -12200,6 +12243,8 @@
|
|
|
12200
12243
|
__publicField$1(this, "crossOriginIframeMirror", new CrossOriginIframeMirror(genId));
|
|
12201
12244
|
__publicField$1(this, "crossOriginIframeStyleMirror");
|
|
12202
12245
|
__publicField$1(this, "crossOriginIframeRootIdMap", /* @__PURE__ */ new WeakMap());
|
|
12246
|
+
__publicField$1(this, "iframeContentDocumentMap", /* @__PURE__ */ new WeakMap());
|
|
12247
|
+
__publicField$1(this, "iframeObserverCleanupMap", /* @__PURE__ */ new WeakMap());
|
|
12203
12248
|
__publicField$1(this, "mirror");
|
|
12204
12249
|
__publicField$1(this, "mutationCb");
|
|
12205
12250
|
__publicField$1(this, "wrappedEmit");
|
|
@@ -12221,6 +12266,31 @@
|
|
|
12221
12266
|
this.iframes.set(iframeEl, true);
|
|
12222
12267
|
if (iframeEl.contentWindow) this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
|
|
12223
12268
|
};
|
|
12269
|
+
_proto.getIframeContentDocument = function getIframeContentDocument(iframeEl) {
|
|
12270
|
+
return this.iframeContentDocumentMap.get(iframeEl);
|
|
12271
|
+
};
|
|
12272
|
+
_proto.setObserverCleanup = function setObserverCleanup(iframeEl, cleanup) {
|
|
12273
|
+
this.iframeObserverCleanupMap.set(iframeEl, cleanup);
|
|
12274
|
+
};
|
|
12275
|
+
_proto.getObserverCleanup = function getObserverCleanup(iframeEl) {
|
|
12276
|
+
return this.iframeObserverCleanupMap.get(iframeEl);
|
|
12277
|
+
};
|
|
12278
|
+
_proto.removeIframe = function removeIframe(iframeEl) {
|
|
12279
|
+
var storedDoc = this.iframeContentDocumentMap.get(iframeEl);
|
|
12280
|
+
if (storedDoc) {
|
|
12281
|
+
this.stylesheetManager.cleanupStylesheetsForRemovedNode(storedDoc);
|
|
12282
|
+
this.mirror.removeNodeFromMap(storedDoc, true);
|
|
12283
|
+
}
|
|
12284
|
+
this.iframes.delete(iframeEl);
|
|
12285
|
+
this.iframeContentDocumentMap.delete(iframeEl);
|
|
12286
|
+
var observerCleanup = this.iframeObserverCleanupMap.get(iframeEl);
|
|
12287
|
+
if (observerCleanup) {
|
|
12288
|
+
try {
|
|
12289
|
+
observerCleanup();
|
|
12290
|
+
} catch (e2) {}
|
|
12291
|
+
this.iframeObserverCleanupMap.delete(iframeEl);
|
|
12292
|
+
}
|
|
12293
|
+
};
|
|
12224
12294
|
_proto.addLoadListener = function addLoadListener(cb) {
|
|
12225
12295
|
this.loadListener = cb;
|
|
12226
12296
|
};
|
|
@@ -12239,6 +12309,9 @@
|
|
|
12239
12309
|
attributes: [],
|
|
12240
12310
|
isAttachIframe: true
|
|
12241
12311
|
});
|
|
12312
|
+
if (iframeEl.contentDocument) {
|
|
12313
|
+
this.iframeContentDocumentMap.set(iframeEl, iframeEl.contentDocument);
|
|
12314
|
+
}
|
|
12242
12315
|
if (this.recordCrossOriginIframes) (_a2 = iframeEl.contentWindow) == null ? void 0 : _a2.addEventListener("message", this.handleMessage.bind(this));
|
|
12243
12316
|
(_b = this.loadListener) == null ? void 0 : _b.call(this, iframeEl);
|
|
12244
12317
|
if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets && iframeEl.contentDocument.adoptedStyleSheets.length > 0) this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
|
|
@@ -13151,6 +13224,41 @@
|
|
|
13151
13224
|
this.styleMirror.reset();
|
|
13152
13225
|
this.trackedLinkElements = /* @__PURE__ */ new WeakSet();
|
|
13153
13226
|
};
|
|
13227
|
+
/**
|
|
13228
|
+
* Cleans up stylesheets associated with a removed node.
|
|
13229
|
+
*
|
|
13230
|
+
* @param removedNode - The node that was removed from the DOM.
|
|
13231
|
+
*/ _proto.cleanupStylesheetsForRemovedNode = function cleanupStylesheetsForRemovedNode(removedNode) {
|
|
13232
|
+
var _this = this;
|
|
13233
|
+
try {
|
|
13234
|
+
if (removedNode.nodeType === Node.DOCUMENT_NODE) {
|
|
13235
|
+
var doc = removedNode;
|
|
13236
|
+
if (doc.adoptedStyleSheets) {
|
|
13237
|
+
for(var _iterator = _create_for_of_iterator_helper_loose(doc.adoptedStyleSheets), _step; !(_step = _iterator()).done;){
|
|
13238
|
+
var sheet = _step.value;
|
|
13239
|
+
this.styleMirror.remove(sheet);
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13243
|
+
if (removedNode.nodeName === "STYLE") {
|
|
13244
|
+
var styleEl = removedNode;
|
|
13245
|
+
if (styleEl.sheet) {
|
|
13246
|
+
this.styleMirror.remove(styleEl.sheet);
|
|
13247
|
+
}
|
|
13248
|
+
}
|
|
13249
|
+
if (removedNode.nodeName === "LINK" && removedNode.rel === "stylesheet") {
|
|
13250
|
+
var linkEl = removedNode;
|
|
13251
|
+
if (linkEl.sheet) {
|
|
13252
|
+
this.styleMirror.remove(linkEl.sheet);
|
|
13253
|
+
}
|
|
13254
|
+
}
|
|
13255
|
+
if (removedNode.childNodes) {
|
|
13256
|
+
removedNode.childNodes.forEach(function(child) {
|
|
13257
|
+
_this.cleanupStylesheetsForRemovedNode(child);
|
|
13258
|
+
});
|
|
13259
|
+
}
|
|
13260
|
+
} catch (e2) {}
|
|
13261
|
+
};
|
|
13154
13262
|
// TODO: take snapshot on stylesheet reload by applying event listener
|
|
13155
13263
|
_proto.trackStylesheetInLinkElement = function trackStylesheetInLinkElement(_linkEl) {};
|
|
13156
13264
|
return StylesheetManager;
|
|
@@ -13605,7 +13713,23 @@
|
|
|
13605
13713
|
};
|
|
13606
13714
|
iframeManager.addLoadListener(function(iframeEl) {
|
|
13607
13715
|
try {
|
|
13608
|
-
|
|
13716
|
+
var iframeDoc = iframeEl.contentDocument;
|
|
13717
|
+
var iframeHandler = observe(iframeDoc);
|
|
13718
|
+
handlers.push(iframeHandler);
|
|
13719
|
+
var existingCleanup = iframeManager.getObserverCleanup(iframeEl);
|
|
13720
|
+
iframeManager.setObserverCleanup(iframeEl, function() {
|
|
13721
|
+
if (existingCleanup) {
|
|
13722
|
+
try {
|
|
13723
|
+
existingCleanup();
|
|
13724
|
+
} catch (e2) {}
|
|
13725
|
+
}
|
|
13726
|
+
try {
|
|
13727
|
+
iframeHandler();
|
|
13728
|
+
var idx = handlers.indexOf(iframeHandler);
|
|
13729
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
13730
|
+
removeMutationBufferForDoc(iframeDoc);
|
|
13731
|
+
} catch (e2) {}
|
|
13732
|
+
});
|
|
13609
13733
|
} catch (error) {
|
|
13610
13734
|
console.warn(error);
|
|
13611
13735
|
}
|
|
@@ -18014,7 +18138,7 @@
|
|
|
18014
18138
|
var __publicField = function(obj, key, value) {
|
|
18015
18139
|
return __defNormalProp(obj, (typeof key === "undefined" ? "undefined" : _type_of(key)) !== "symbol" ? key + "" : key, value);
|
|
18016
18140
|
};
|
|
18017
|
-
function patch(source, name, replacement) {
|
|
18141
|
+
function patch$3(source, name, replacement) {
|
|
18018
18142
|
try {
|
|
18019
18143
|
if (!(name in source)) {
|
|
18020
18144
|
return function() {};
|
|
@@ -18431,7 +18555,7 @@
|
|
|
18431
18555
|
if (!_logger[level]) {
|
|
18432
18556
|
return function() {};
|
|
18433
18557
|
}
|
|
18434
|
-
return patch(_logger, level, function(original) {
|
|
18558
|
+
return patch$3(_logger, level, function(original) {
|
|
18435
18559
|
var _this1 = _this;
|
|
18436
18560
|
return function() {
|
|
18437
18561
|
for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
|
|
@@ -18852,11 +18976,6 @@
|
|
|
18852
18976
|
PromisePolyfill = NpoPromise;
|
|
18853
18977
|
}
|
|
18854
18978
|
|
|
18855
|
-
var Config = {
|
|
18856
|
-
DEBUG: false,
|
|
18857
|
-
LIB_VERSION: '2.74.0'
|
|
18858
|
-
};
|
|
18859
|
-
|
|
18860
18979
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
18861
18980
|
|
|
18862
18981
|
// Maximum allowed session recording length
|
|
@@ -19060,15 +19179,8 @@
|
|
|
19060
19179
|
return toString.call(obj) === '[object Array]';
|
|
19061
19180
|
};
|
|
19062
19181
|
|
|
19063
|
-
// from a comment on http://dbj.org/dbj/?p=286
|
|
19064
|
-
// fails on only one very rare and deliberate custom object:
|
|
19065
|
-
// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
|
|
19066
19182
|
_.isFunction = function(f) {
|
|
19067
|
-
|
|
19068
|
-
return /^\s*\bfunction\b/.test(f);
|
|
19069
|
-
} catch (x) {
|
|
19070
|
-
return false;
|
|
19071
|
-
}
|
|
19183
|
+
return typeof f === 'function';
|
|
19072
19184
|
};
|
|
19073
19185
|
|
|
19074
19186
|
_.isArguments = function(obj) {
|
|
@@ -20595,6 +20707,17 @@
|
|
|
20595
20707
|
|
|
20596
20708
|
var NOOP_FUNC = function () {};
|
|
20597
20709
|
|
|
20710
|
+
var urlMatchesRegexList = function (url, regexList) {
|
|
20711
|
+
var matches = false;
|
|
20712
|
+
for (var i = 0; i < regexList.length; i++) {
|
|
20713
|
+
if (url.match(regexList[i])) {
|
|
20714
|
+
matches = true;
|
|
20715
|
+
break;
|
|
20716
|
+
}
|
|
20717
|
+
}
|
|
20718
|
+
return matches;
|
|
20719
|
+
};
|
|
20720
|
+
|
|
20598
20721
|
var JSONStringify = null, JSONParse = null;
|
|
20599
20722
|
if (typeof JSON !== 'undefined') {
|
|
20600
20723
|
JSONStringify = JSON.stringify;
|
|
@@ -21066,7 +21189,7 @@
|
|
|
21066
21189
|
};
|
|
21067
21190
|
}
|
|
21068
21191
|
|
|
21069
|
-
var logger$
|
|
21192
|
+
var logger$7 = console_with_prefix('lock');
|
|
21070
21193
|
|
|
21071
21194
|
/**
|
|
21072
21195
|
* SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
|
|
@@ -21118,7 +21241,7 @@
|
|
|
21118
21241
|
|
|
21119
21242
|
var delay = function(cb) {
|
|
21120
21243
|
if (new Date().getTime() - startTime > timeoutMS) {
|
|
21121
|
-
logger$
|
|
21244
|
+
logger$7.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
|
|
21122
21245
|
storage.removeItem(keyZ);
|
|
21123
21246
|
storage.removeItem(keyY);
|
|
21124
21247
|
loop();
|
|
@@ -21265,7 +21388,7 @@
|
|
|
21265
21388
|
}, this));
|
|
21266
21389
|
};
|
|
21267
21390
|
|
|
21268
|
-
var logger$
|
|
21391
|
+
var logger$6 = console_with_prefix('batch');
|
|
21269
21392
|
|
|
21270
21393
|
/**
|
|
21271
21394
|
* RequestQueue: queue for batching API requests with localStorage backup for retries.
|
|
@@ -21294,7 +21417,7 @@
|
|
|
21294
21417
|
timeoutMS: options.sharedLockTimeoutMS,
|
|
21295
21418
|
});
|
|
21296
21419
|
}
|
|
21297
|
-
this.reportError = options.errorReporter || _.bind(logger$
|
|
21420
|
+
this.reportError = options.errorReporter || _.bind(logger$6.error, logger$6);
|
|
21298
21421
|
|
|
21299
21422
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
21300
21423
|
|
|
@@ -21627,7 +21750,7 @@
|
|
|
21627
21750
|
// maximum interval between request retries after exponential backoff
|
|
21628
21751
|
var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
21629
21752
|
|
|
21630
|
-
var logger$
|
|
21753
|
+
var logger$5 = console_with_prefix('batch');
|
|
21631
21754
|
|
|
21632
21755
|
/**
|
|
21633
21756
|
* RequestBatcher: manages the queueing, flushing, retry etc of requests of one
|
|
@@ -21755,7 +21878,7 @@
|
|
|
21755
21878
|
*/
|
|
21756
21879
|
RequestBatcher.prototype.flush = function(options) {
|
|
21757
21880
|
if (this.requestInProgress) {
|
|
21758
|
-
logger$
|
|
21881
|
+
logger$5.log('Flush: Request already in progress');
|
|
21759
21882
|
return PromisePolyfill.resolve();
|
|
21760
21883
|
}
|
|
21761
21884
|
|
|
@@ -21932,7 +22055,7 @@
|
|
|
21932
22055
|
if (options.unloading) {
|
|
21933
22056
|
requestOptions.transport = 'sendBeacon';
|
|
21934
22057
|
}
|
|
21935
|
-
logger$
|
|
22058
|
+
logger$5.log('MIXPANEL REQUEST:', dataForRequest);
|
|
21936
22059
|
return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
|
|
21937
22060
|
}, this))
|
|
21938
22061
|
.catch(_.bind(function(err) {
|
|
@@ -21945,7 +22068,7 @@
|
|
|
21945
22068
|
* Log error to global logger and optional user-defined logger.
|
|
21946
22069
|
*/
|
|
21947
22070
|
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
21948
|
-
logger$
|
|
22071
|
+
logger$5.error.apply(logger$5.error, arguments);
|
|
21949
22072
|
if (this.errorReporter) {
|
|
21950
22073
|
try {
|
|
21951
22074
|
if (!(err instanceof Error)) {
|
|
@@ -21953,7 +22076,7 @@
|
|
|
21953
22076
|
}
|
|
21954
22077
|
this.errorReporter(msg, err);
|
|
21955
22078
|
} catch(err) {
|
|
21956
|
-
logger$
|
|
22079
|
+
logger$5.error(err);
|
|
21957
22080
|
}
|
|
21958
22081
|
}
|
|
21959
22082
|
};
|
|
@@ -22075,7 +22198,7 @@
|
|
|
22075
22198
|
|
|
22076
22199
|
var MAX_DEPTH = 5;
|
|
22077
22200
|
|
|
22078
|
-
var logger$
|
|
22201
|
+
var logger$4 = console_with_prefix('autocapture');
|
|
22079
22202
|
|
|
22080
22203
|
|
|
22081
22204
|
function getClasses(el) {
|
|
@@ -22339,7 +22462,7 @@
|
|
|
22339
22462
|
return false;
|
|
22340
22463
|
}
|
|
22341
22464
|
} catch (err) {
|
|
22342
|
-
logger$
|
|
22465
|
+
logger$4.critical('Error while checking element in allowElementCallback', err);
|
|
22343
22466
|
return false;
|
|
22344
22467
|
}
|
|
22345
22468
|
}
|
|
@@ -22356,7 +22479,7 @@
|
|
|
22356
22479
|
return true;
|
|
22357
22480
|
}
|
|
22358
22481
|
} catch (err) {
|
|
22359
|
-
logger$
|
|
22482
|
+
logger$4.critical('Error while checking selector: ' + sel, err);
|
|
22360
22483
|
}
|
|
22361
22484
|
}
|
|
22362
22485
|
return false;
|
|
@@ -22371,7 +22494,7 @@
|
|
|
22371
22494
|
return true;
|
|
22372
22495
|
}
|
|
22373
22496
|
} catch (err) {
|
|
22374
|
-
logger$
|
|
22497
|
+
logger$4.critical('Error while checking element in blockElementCallback', err);
|
|
22375
22498
|
return true;
|
|
22376
22499
|
}
|
|
22377
22500
|
}
|
|
@@ -22385,7 +22508,7 @@
|
|
|
22385
22508
|
return true;
|
|
22386
22509
|
}
|
|
22387
22510
|
} catch (err) {
|
|
22388
|
-
logger$
|
|
22511
|
+
logger$4.critical('Error while checking selector: ' + sel, err);
|
|
22389
22512
|
}
|
|
22390
22513
|
}
|
|
22391
22514
|
}
|
|
@@ -22933,173 +23056,822 @@
|
|
|
22933
23056
|
}
|
|
22934
23057
|
|
|
22935
23058
|
/**
|
|
22936
|
-
*
|
|
23059
|
+
* This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
|
|
23060
|
+
* the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
|
|
23061
|
+
*
|
|
23062
|
+
* This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
|
|
23063
|
+
* header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
|
|
23064
|
+
*
|
|
22937
23065
|
*/
|
|
22938
23066
|
|
|
23067
|
+
var logger$3 = console_with_prefix('network-plugin');
|
|
22939
23068
|
|
|
22940
|
-
|
|
22941
|
-
|
|
22942
|
-
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
23069
|
+
/**
|
|
23070
|
+
* Get the time origin for converting performance timestamps to absolute timestamps.
|
|
23071
|
+
* Uses Date.now() - performance.now() instead of performance.timeOrigin because
|
|
23072
|
+
* browsers can report timeOrigin values that are skewed from actual time, and some
|
|
23073
|
+
* browsers (notably older Safari versions) don't implement timeOrigin at all.
|
|
23074
|
+
* See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
|
|
23075
|
+
* @param {Window} win
|
|
23076
|
+
* @returns {number}
|
|
23077
|
+
*/
|
|
23078
|
+
function getTimeOrigin(win) {
|
|
23079
|
+
return Math.round(Date.now() - win.performance.now());
|
|
23080
|
+
}
|
|
22949
23081
|
|
|
22950
|
-
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
IncrementalSource.TouchMove,
|
|
22957
|
-
IncrementalSource.MediaInteraction,
|
|
22958
|
-
IncrementalSource.Drag,
|
|
22959
|
-
IncrementalSource.Selection,
|
|
22960
|
-
]);
|
|
23082
|
+
/**
|
|
23083
|
+
* @typedef {import('../index.d.ts').InitiatorType} InitiatorType
|
|
23084
|
+
* @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
|
|
23085
|
+
* @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
|
|
23086
|
+
* @typedef {import('../index.d.ts').NetworkData} NetworkData
|
|
23087
|
+
*/
|
|
22961
23088
|
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
23089
|
+
/**
|
|
23090
|
+
* @typedef {Record<string, string>} Headers
|
|
23091
|
+
*/
|
|
22965
23092
|
|
|
22966
23093
|
/**
|
|
22967
|
-
* @typedef {
|
|
22968
|
-
* @property {number} idleExpires
|
|
22969
|
-
* @property {number} maxExpires
|
|
22970
|
-
* @property {number} replayStartTime
|
|
22971
|
-
* @property {number} lastEventTimestamp
|
|
22972
|
-
* @property {number} seqNo
|
|
22973
|
-
* @property {string} batchStartUrl
|
|
22974
|
-
* @property {string} replayId
|
|
22975
|
-
* @property {string} tabId
|
|
22976
|
-
* @property {string} replayStartUrl
|
|
23094
|
+
* @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
|
|
22977
23095
|
*/
|
|
22978
23096
|
|
|
22979
23097
|
/**
|
|
22980
|
-
* @
|
|
22981
|
-
* @
|
|
22982
|
-
* @
|
|
22983
|
-
* @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
22984
|
-
* @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
22985
|
-
* @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
|
|
22986
|
-
* @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
|
|
22987
|
-
* @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
|
|
22988
|
-
* optional properties for deserialization:
|
|
22989
|
-
* @property {number} idleExpires
|
|
22990
|
-
* @property {number} maxExpires
|
|
22991
|
-
* @property {number} replayStartTime
|
|
22992
|
-
* @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
|
|
22993
|
-
* @property {number} seqNo
|
|
22994
|
-
* @property {string} batchStartUrl
|
|
22995
|
-
* @property {string} replayStartUrl
|
|
23098
|
+
* @callback networkCallback
|
|
23099
|
+
* @param {NetworkData} data
|
|
23100
|
+
* @returns {void}
|
|
22996
23101
|
*/
|
|
22997
23102
|
|
|
22998
23103
|
/**
|
|
22999
|
-
* @
|
|
23000
|
-
* @
|
|
23001
|
-
* @property {string} user_id
|
|
23002
|
-
* @property {string} device_id
|
|
23104
|
+
* @callback listenerHandler
|
|
23105
|
+
* @returns {void}
|
|
23003
23106
|
*/
|
|
23004
23107
|
|
|
23108
|
+
/**
|
|
23109
|
+
* @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
|
|
23110
|
+
*/
|
|
23005
23111
|
|
|
23006
23112
|
/**
|
|
23007
|
-
*
|
|
23008
|
-
* @
|
|
23113
|
+
* @typedef {Object} RecordPlugin
|
|
23114
|
+
* @property {string} name
|
|
23115
|
+
* @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
|
|
23116
|
+
* @property {NetworkRecordOptions} [options]
|
|
23009
23117
|
*/
|
|
23010
|
-
var SessionRecording = function(options) {
|
|
23011
|
-
this._mixpanel = options.mixpanelInstance;
|
|
23012
|
-
this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
|
|
23013
|
-
this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
|
|
23014
|
-
this._onBatchSent = options.onBatchSent || NOOP_FUNC;
|
|
23015
|
-
this._rrwebRecord = options.rrwebRecord || null;
|
|
23016
23118
|
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
|
|
23119
|
+
/** @type {Required<NetworkRecordOptions>} */
|
|
23120
|
+
var defaultNetworkOptions = {
|
|
23121
|
+
initiatorTypes: [
|
|
23122
|
+
'audio',
|
|
23123
|
+
'beacon',
|
|
23124
|
+
'body',
|
|
23125
|
+
'css',
|
|
23126
|
+
'early-hint',
|
|
23127
|
+
'embed',
|
|
23128
|
+
'fetch',
|
|
23129
|
+
'frame',
|
|
23130
|
+
'iframe',
|
|
23131
|
+
'icon',
|
|
23132
|
+
'image',
|
|
23133
|
+
'img',
|
|
23134
|
+
'input',
|
|
23135
|
+
'link',
|
|
23136
|
+
'navigation',
|
|
23137
|
+
'object',
|
|
23138
|
+
'ping',
|
|
23139
|
+
'script',
|
|
23140
|
+
'track',
|
|
23141
|
+
'video',
|
|
23142
|
+
'xmlhttprequest',
|
|
23143
|
+
],
|
|
23144
|
+
ignoreRequestFn: function() { return false; },
|
|
23145
|
+
recordHeaders: {
|
|
23146
|
+
request: [],
|
|
23147
|
+
response: [],
|
|
23148
|
+
},
|
|
23149
|
+
recordBodyUrls: {
|
|
23150
|
+
request: [],
|
|
23151
|
+
response: [],
|
|
23152
|
+
},
|
|
23153
|
+
recordInitialRequests: false,
|
|
23154
|
+
};
|
|
23020
23155
|
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
|
|
23026
|
-
|
|
23027
|
-
|
|
23156
|
+
/**
|
|
23157
|
+
* @param {PerformanceEntry} entry
|
|
23158
|
+
* @returns {entry is PerformanceNavigationTiming}
|
|
23159
|
+
*/
|
|
23160
|
+
function isNavigationTiming(entry) {
|
|
23161
|
+
return entry.entryType === 'navigation';
|
|
23162
|
+
}
|
|
23028
23163
|
|
|
23029
|
-
|
|
23030
|
-
|
|
23164
|
+
/**
|
|
23165
|
+
* @param {PerformanceEntry} entry
|
|
23166
|
+
* @returns {entry is PerformanceResourceTiming}
|
|
23167
|
+
*/
|
|
23168
|
+
function isResourceTiming (entry) {
|
|
23169
|
+
return entry.entryType === 'resource';
|
|
23170
|
+
}
|
|
23031
23171
|
|
|
23032
|
-
|
|
23033
|
-
|
|
23172
|
+
function findLast(array, predicate) {
|
|
23173
|
+
var length = array.length;
|
|
23174
|
+
for (var i = length - 1; i >= 0; i -= 1) {
|
|
23175
|
+
if (predicate(array[i])) {
|
|
23176
|
+
return array[i];
|
|
23177
|
+
}
|
|
23178
|
+
}
|
|
23179
|
+
}
|
|
23034
23180
|
|
|
23035
|
-
|
|
23036
|
-
|
|
23037
|
-
|
|
23181
|
+
/**
|
|
23182
|
+
* Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
|
|
23183
|
+
* Adapted from Sentry's `fill` utility:
|
|
23184
|
+
* https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
|
|
23185
|
+
*
|
|
23186
|
+
* @param {object} source - The object containing the method to patch
|
|
23187
|
+
* @param {string} name - The method name to patch
|
|
23188
|
+
* @param {function} replacementFactory - A function that receives the original method and returns the replacement
|
|
23189
|
+
* @returns {function} A function that restores the original method
|
|
23190
|
+
*/
|
|
23191
|
+
function patch(source, name, replacementFactory) {
|
|
23192
|
+
if (!(name in source) || typeof source[name] !== 'function') {
|
|
23193
|
+
return function() {};
|
|
23194
|
+
}
|
|
23195
|
+
var original = source[name];
|
|
23196
|
+
var wrapped = replacementFactory(original);
|
|
23197
|
+
source[name] = wrapped;
|
|
23198
|
+
return function() {
|
|
23199
|
+
source[name] = original;
|
|
23200
|
+
};
|
|
23201
|
+
}
|
|
23038
23202
|
|
|
23039
|
-
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
23040
|
-
this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
|
|
23041
|
-
this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
|
|
23042
|
-
this.batcher = new RequestBatcher(this.batcherKey, {
|
|
23043
|
-
errorReporter: this.reportError.bind(this),
|
|
23044
|
-
flushOnlyOnInterval: true,
|
|
23045
|
-
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
23046
|
-
sendRequestFunc: this.flushEventsWithOptOut.bind(this),
|
|
23047
|
-
queueStorage: this.queueStorage,
|
|
23048
|
-
sharedLockStorage: options.sharedLockStorage,
|
|
23049
|
-
usePersistence: usePersistence,
|
|
23050
|
-
stopAllBatchingFunc: this.stopRecording.bind(this),
|
|
23051
23203
|
|
|
23052
|
-
|
|
23053
|
-
|
|
23054
|
-
|
|
23055
|
-
|
|
23056
|
-
sharedLockTimeoutMS: 10 * 1000,
|
|
23057
|
-
});
|
|
23058
|
-
};
|
|
23204
|
+
/**
|
|
23205
|
+
* Maximum body size to record (1MB)
|
|
23206
|
+
*/
|
|
23207
|
+
var MAX_BODY_SIZE = 1024 * 1024;
|
|
23059
23208
|
|
|
23060
23209
|
/**
|
|
23061
|
-
*
|
|
23210
|
+
* Truncate string if it exceeds max size
|
|
23211
|
+
* @param {string} str
|
|
23212
|
+
* @returns {string}
|
|
23062
23213
|
*/
|
|
23063
|
-
|
|
23064
|
-
if (
|
|
23065
|
-
return
|
|
23214
|
+
function truncateBody(str) {
|
|
23215
|
+
if (!str || typeof str !== 'string') {
|
|
23216
|
+
return str;
|
|
23217
|
+
}
|
|
23218
|
+
if (str.length > MAX_BODY_SIZE) {
|
|
23219
|
+
logger$3.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
|
|
23220
|
+
return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
|
|
23066
23221
|
}
|
|
23222
|
+
return str;
|
|
23223
|
+
}
|
|
23067
23224
|
|
|
23068
|
-
|
|
23069
|
-
|
|
23225
|
+
/**
|
|
23226
|
+
* @param {networkCallback} cb
|
|
23227
|
+
* @param {Window} win
|
|
23228
|
+
* @param {Required<NetworkRecordOptions>} options
|
|
23229
|
+
* @returns {listenerHandler}
|
|
23230
|
+
*/
|
|
23231
|
+
function initPerformanceObserver(cb, win, options) {
|
|
23232
|
+
if (!win.PerformanceObserver) {
|
|
23233
|
+
logger$3.error('PerformanceObserver not supported');
|
|
23234
|
+
return function() {
|
|
23235
|
+
//
|
|
23236
|
+
};
|
|
23237
|
+
}
|
|
23238
|
+
if (options.recordInitialRequests) {
|
|
23239
|
+
var initialPerformanceEntries = win.performance
|
|
23240
|
+
.getEntries()
|
|
23241
|
+
.filter(function(entry) {
|
|
23242
|
+
return isNavigationTiming(entry) ||
|
|
23243
|
+
(isResourceTiming(entry) &&
|
|
23244
|
+
options.initiatorTypes.includes(entry.initiatorType));
|
|
23245
|
+
});
|
|
23246
|
+
cb({
|
|
23247
|
+
requests: initialPerformanceEntries.map(function(entry) {
|
|
23248
|
+
return {
|
|
23249
|
+
url: entry.name,
|
|
23250
|
+
initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
|
|
23251
|
+
status: 'responseStatus' in entry ? entry.responseStatus : undefined,
|
|
23252
|
+
startTime: Math.round(entry.startTime),
|
|
23253
|
+
endTime: Math.round(entry.responseEnd),
|
|
23254
|
+
timeOrigin: getTimeOrigin(win),
|
|
23255
|
+
};
|
|
23256
|
+
}),
|
|
23257
|
+
isInitial: true,
|
|
23258
|
+
});
|
|
23259
|
+
}
|
|
23260
|
+
var observer = new win.PerformanceObserver(function(entries) {
|
|
23261
|
+
var performanceEntries = entries
|
|
23262
|
+
.getEntries()
|
|
23263
|
+
.filter(function(entry) {
|
|
23264
|
+
return isResourceTiming(entry) &&
|
|
23265
|
+
options.initiatorTypes.includes(entry.initiatorType) &&
|
|
23266
|
+
entry.initiatorType !== 'xmlhttprequest' &&
|
|
23267
|
+
entry.initiatorType !== 'fetch';
|
|
23268
|
+
});
|
|
23269
|
+
cb({
|
|
23270
|
+
requests: performanceEntries.map(function(entry) {
|
|
23271
|
+
return {
|
|
23272
|
+
url: entry.name,
|
|
23273
|
+
initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
|
|
23274
|
+
status: 'responseStatus' in entry ? entry.responseStatus : undefined,
|
|
23275
|
+
startTime: Math.round(entry.startTime),
|
|
23276
|
+
endTime: Math.round(entry.responseEnd),
|
|
23277
|
+
timeOrigin: getTimeOrigin(win),
|
|
23278
|
+
};
|
|
23279
|
+
}),
|
|
23280
|
+
});
|
|
23281
|
+
});
|
|
23282
|
+
observer.observe({ entryTypes: ['navigation', 'resource'] });
|
|
23283
|
+
return function() {
|
|
23284
|
+
observer.disconnect();
|
|
23070
23285
|
};
|
|
23286
|
+
}
|
|
23071
23287
|
|
|
23072
|
-
|
|
23073
|
-
|
|
23074
|
-
|
|
23075
|
-
|
|
23288
|
+
/**
|
|
23289
|
+
* Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
|
|
23290
|
+
* @param {'request' | 'response'} type
|
|
23291
|
+
* @param {NetworkRecordOptions['recordHeaders']} recordHeaders
|
|
23292
|
+
* @param {string} headerName
|
|
23293
|
+
* @returns {boolean}
|
|
23294
|
+
*/
|
|
23295
|
+
function shouldRecordHeader(type, recordHeaders, headerName) {
|
|
23296
|
+
if (!recordHeaders[type] || recordHeaders[type].length === 0) {
|
|
23297
|
+
return false;
|
|
23076
23298
|
}
|
|
23077
|
-
|
|
23078
|
-
|
|
23079
|
-
|
|
23299
|
+
|
|
23300
|
+
return recordHeaders[type].includes(headerName.toLowerCase());
|
|
23301
|
+
}
|
|
23302
|
+
|
|
23303
|
+
/**
|
|
23304
|
+
* Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
|
|
23305
|
+
* @param {'request' | 'response'} type
|
|
23306
|
+
* @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
|
|
23307
|
+
* @param {string} url
|
|
23308
|
+
* @returns {boolean}
|
|
23309
|
+
*/
|
|
23310
|
+
function shouldRecordBody(type, recordBodyUrls, url) {
|
|
23311
|
+
if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
|
|
23312
|
+
return false;
|
|
23080
23313
|
}
|
|
23081
|
-
return userIdInfo;
|
|
23082
|
-
};
|
|
23083
23314
|
|
|
23084
|
-
|
|
23085
|
-
|
|
23315
|
+
return urlMatchesRegexList(url, recordBodyUrls[type]);
|
|
23316
|
+
}
|
|
23086
23317
|
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23091
|
-
|
|
23092
|
-
|
|
23318
|
+
function tryReadXHRBody(body) {
|
|
23319
|
+
if (body === null || body === undefined) {
|
|
23320
|
+
return null;
|
|
23321
|
+
}
|
|
23322
|
+
|
|
23323
|
+
var result;
|
|
23324
|
+
if (typeof body === 'string') {
|
|
23325
|
+
result = body;
|
|
23326
|
+
} else if (body instanceof Document) {
|
|
23327
|
+
result = body.textContent;
|
|
23328
|
+
} else if (body instanceof FormData) {
|
|
23329
|
+
result = _.HTTPBuildQuery(body);
|
|
23330
|
+
} else if (_.isObject(body)) {
|
|
23331
|
+
try {
|
|
23332
|
+
result = JSON.stringify(body);
|
|
23333
|
+
} catch (e) {
|
|
23334
|
+
return 'Failed to stringify response object';
|
|
23093
23335
|
}
|
|
23336
|
+
} else {
|
|
23337
|
+
return 'Cannot read body of type ' + typeof body;
|
|
23338
|
+
}
|
|
23094
23339
|
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
return this.queueStorage.removeItem(this.batcherKey);
|
|
23098
|
-
}.bind(this));
|
|
23099
|
-
}.bind(this));
|
|
23100
|
-
};
|
|
23340
|
+
return truncateBody(result);
|
|
23341
|
+
}
|
|
23101
23342
|
|
|
23102
|
-
|
|
23343
|
+
/**
|
|
23344
|
+
* @param {Request | Response} r
|
|
23345
|
+
* @returns {Promise<string>}
|
|
23346
|
+
*/
|
|
23347
|
+
function tryReadFetchBody(r) {
|
|
23348
|
+
return new Promise(function(resolve) {
|
|
23349
|
+
var timeout = setTimeout(function() {
|
|
23350
|
+
resolve('Timeout while trying to read body');
|
|
23351
|
+
}, 500);
|
|
23352
|
+
try {
|
|
23353
|
+
r.clone()
|
|
23354
|
+
.text()
|
|
23355
|
+
.then(
|
|
23356
|
+
function(txt) {
|
|
23357
|
+
clearTimeout(timeout);
|
|
23358
|
+
resolve(truncateBody(txt));
|
|
23359
|
+
},
|
|
23360
|
+
function(reason) {
|
|
23361
|
+
clearTimeout(timeout);
|
|
23362
|
+
resolve('Failed to read body: ' + String(reason));
|
|
23363
|
+
}
|
|
23364
|
+
);
|
|
23365
|
+
} catch (e) {
|
|
23366
|
+
clearTimeout(timeout);
|
|
23367
|
+
resolve('Failed to read body: ' + String(e));
|
|
23368
|
+
}
|
|
23369
|
+
});
|
|
23370
|
+
}
|
|
23371
|
+
|
|
23372
|
+
/**
|
|
23373
|
+
* @param {Window} win
|
|
23374
|
+
* @param {string} initiatorType
|
|
23375
|
+
* @param {string} url
|
|
23376
|
+
* @param {number} [after]
|
|
23377
|
+
* @param {number} [before]
|
|
23378
|
+
* @param {number} [attempt]
|
|
23379
|
+
* @returns {Promise<PerformanceResourceTiming>}
|
|
23380
|
+
*/
|
|
23381
|
+
function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
|
|
23382
|
+
if (attempt === undefined) {
|
|
23383
|
+
attempt = 0;
|
|
23384
|
+
}
|
|
23385
|
+
if (attempt > 10) {
|
|
23386
|
+
logger$3.error('Cannot find performance entry');
|
|
23387
|
+
return Promise.resolve(null);
|
|
23388
|
+
}
|
|
23389
|
+
var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
|
|
23390
|
+
win.performance.getEntriesByName(url)
|
|
23391
|
+
);
|
|
23392
|
+
var performanceEntry = findLast(
|
|
23393
|
+
urlPerformanceEntries,
|
|
23394
|
+
function(entry) {
|
|
23395
|
+
return isResourceTiming(entry) &&
|
|
23396
|
+
entry.initiatorType === initiatorType &&
|
|
23397
|
+
(!after || entry.startTime >= after) &&
|
|
23398
|
+
(!before || entry.startTime <= before);
|
|
23399
|
+
}
|
|
23400
|
+
);
|
|
23401
|
+
if (!performanceEntry) {
|
|
23402
|
+
return new Promise(function(resolve) {
|
|
23403
|
+
setTimeout(resolve, 50 * attempt);
|
|
23404
|
+
}).then(function() {
|
|
23405
|
+
return getRequestPerformanceEntry(
|
|
23406
|
+
win,
|
|
23407
|
+
initiatorType,
|
|
23408
|
+
url,
|
|
23409
|
+
after,
|
|
23410
|
+
before,
|
|
23411
|
+
attempt + 1
|
|
23412
|
+
);
|
|
23413
|
+
});
|
|
23414
|
+
}
|
|
23415
|
+
return Promise.resolve(performanceEntry);
|
|
23416
|
+
}
|
|
23417
|
+
|
|
23418
|
+
/**
|
|
23419
|
+
* @param {networkCallback} cb
|
|
23420
|
+
* @param {Window} win
|
|
23421
|
+
* @param {Required<NetworkRecordOptions>} options
|
|
23422
|
+
* @returns {listenerHandler}
|
|
23423
|
+
*/
|
|
23424
|
+
function initXhrObserver(cb, win, options) {
|
|
23425
|
+
if (!options.initiatorTypes.includes('xmlhttprequest')) {
|
|
23426
|
+
return function() {
|
|
23427
|
+
//
|
|
23428
|
+
};
|
|
23429
|
+
}
|
|
23430
|
+
var restorePatch = patch(
|
|
23431
|
+
win.XMLHttpRequest.prototype,
|
|
23432
|
+
'open',
|
|
23433
|
+
function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
|
|
23434
|
+
return function(
|
|
23435
|
+
/** @type {string} */ method,
|
|
23436
|
+
/** @type {string | URL} */ url,
|
|
23437
|
+
/** @type {boolean} */ async,
|
|
23438
|
+
username, password
|
|
23439
|
+
) {
|
|
23440
|
+
if (async === undefined) {
|
|
23441
|
+
async = true;
|
|
23442
|
+
}
|
|
23443
|
+
var xhr = /** @type {XMLHttpRequest} */ (this);
|
|
23444
|
+
var req = new Request(url, { method: method });
|
|
23445
|
+
/** @type {Partial<NetworkRequest>} */
|
|
23446
|
+
var networkRequest = {};
|
|
23447
|
+
/** @type {number | undefined} */
|
|
23448
|
+
var after;
|
|
23449
|
+
/** @type {number | undefined} */
|
|
23450
|
+
var before;
|
|
23451
|
+
|
|
23452
|
+
/** @type {Headers} */
|
|
23453
|
+
var requestHeaders = {};
|
|
23454
|
+
var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
|
|
23455
|
+
xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
|
|
23456
|
+
if (shouldRecordHeader('request', options.recordHeaders, header)) {
|
|
23457
|
+
requestHeaders[header] = value;
|
|
23458
|
+
}
|
|
23459
|
+
return originalSetRequestHeader(header, value);
|
|
23460
|
+
};
|
|
23461
|
+
networkRequest.requestHeaders = requestHeaders;
|
|
23462
|
+
|
|
23463
|
+
var originalSend = xhr.send.bind(xhr);
|
|
23464
|
+
xhr.send = function(/** @type {Body} */ body) {
|
|
23465
|
+
if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
|
|
23466
|
+
networkRequest.requestBody = tryReadXHRBody(body);
|
|
23467
|
+
}
|
|
23468
|
+
after = win.performance.now();
|
|
23469
|
+
return originalSend(body);
|
|
23470
|
+
};
|
|
23471
|
+
xhr.addEventListener('readystatechange', function() {
|
|
23472
|
+
if (xhr.readyState !== xhr.DONE) {
|
|
23473
|
+
return;
|
|
23474
|
+
}
|
|
23475
|
+
before = win.performance.now();
|
|
23476
|
+
/** @type {Headers} */
|
|
23477
|
+
var responseHeaders = {};
|
|
23478
|
+
var rawHeaders = xhr.getAllResponseHeaders();
|
|
23479
|
+
if (rawHeaders) {
|
|
23480
|
+
var headers = rawHeaders.trim().split(/[\r\n]+/);
|
|
23481
|
+
headers.forEach(function(line) {
|
|
23482
|
+
if (!line) return;
|
|
23483
|
+
var colonIndex = line.indexOf(': ');
|
|
23484
|
+
if (colonIndex === -1) return;
|
|
23485
|
+
var header = line.substring(0, colonIndex);
|
|
23486
|
+
var value = line.substring(colonIndex + 2);
|
|
23487
|
+
if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
|
|
23488
|
+
responseHeaders[header] = value;
|
|
23489
|
+
}
|
|
23490
|
+
});
|
|
23491
|
+
}
|
|
23492
|
+
networkRequest.responseHeaders = responseHeaders;
|
|
23493
|
+
if (
|
|
23494
|
+
shouldRecordBody('response', options.recordBodyUrls, req.url)
|
|
23495
|
+
) {
|
|
23496
|
+
networkRequest.responseBody = tryReadXHRBody(xhr.response);
|
|
23497
|
+
}
|
|
23498
|
+
getRequestPerformanceEntry(
|
|
23499
|
+
win,
|
|
23500
|
+
'xmlhttprequest',
|
|
23501
|
+
req.url,
|
|
23502
|
+
after,
|
|
23503
|
+
before
|
|
23504
|
+
)
|
|
23505
|
+
.then(function(entry) {
|
|
23506
|
+
if (!entry) {
|
|
23507
|
+
logger$3.error('Failed to get performance entry for XHR request to ' + req.url);
|
|
23508
|
+
return;
|
|
23509
|
+
}
|
|
23510
|
+
/** @type {NetworkRequest} */
|
|
23511
|
+
var request = {
|
|
23512
|
+
url: entry.name,
|
|
23513
|
+
method: req.method,
|
|
23514
|
+
initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
|
|
23515
|
+
status: xhr.status,
|
|
23516
|
+
startTime: Math.round(entry.startTime),
|
|
23517
|
+
endTime: Math.round(entry.responseEnd),
|
|
23518
|
+
timeOrigin: getTimeOrigin(win),
|
|
23519
|
+
requestHeaders: networkRequest.requestHeaders,
|
|
23520
|
+
requestBody: networkRequest.requestBody,
|
|
23521
|
+
responseHeaders: networkRequest.responseHeaders,
|
|
23522
|
+
responseBody: networkRequest.responseBody,
|
|
23523
|
+
};
|
|
23524
|
+
cb({ requests: [request] });
|
|
23525
|
+
})
|
|
23526
|
+
.catch(function(e) {
|
|
23527
|
+
logger$3.error('Error recording XHR request to ' + req.url + ': ' + String(e));
|
|
23528
|
+
});
|
|
23529
|
+
});
|
|
23530
|
+
|
|
23531
|
+
originalOpen.call(xhr, method, url, async, username, password);
|
|
23532
|
+
};
|
|
23533
|
+
}
|
|
23534
|
+
);
|
|
23535
|
+
return function() {
|
|
23536
|
+
restorePatch();
|
|
23537
|
+
};
|
|
23538
|
+
}
|
|
23539
|
+
|
|
23540
|
+
/**
|
|
23541
|
+
* @param {networkCallback} cb
|
|
23542
|
+
* @param {Window} win
|
|
23543
|
+
* @param {Required<NetworkRecordOptions>} options
|
|
23544
|
+
* @returns {listenerHandler}
|
|
23545
|
+
*/
|
|
23546
|
+
function initFetchObserver(cb, win, options) {
|
|
23547
|
+
if (!options.initiatorTypes.includes('fetch')) {
|
|
23548
|
+
return function() {
|
|
23549
|
+
//
|
|
23550
|
+
};
|
|
23551
|
+
}
|
|
23552
|
+
|
|
23553
|
+
var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
|
|
23554
|
+
return function() {
|
|
23555
|
+
var req = new Request(arguments[0], arguments[1]);
|
|
23556
|
+
/** @type {Response | undefined} */
|
|
23557
|
+
var res;
|
|
23558
|
+
/** @type {Partial<NetworkRequest>} */
|
|
23559
|
+
var networkRequest = {};
|
|
23560
|
+
/** @type {number | undefined} */
|
|
23561
|
+
var after;
|
|
23562
|
+
/** @type {number | undefined} */
|
|
23563
|
+
var before;
|
|
23564
|
+
|
|
23565
|
+
var originalFetchPromise;
|
|
23566
|
+
var requestBodyPromise = Promise.resolve(undefined);
|
|
23567
|
+
var responseBodyPromise = Promise.resolve(undefined);
|
|
23568
|
+
try {
|
|
23569
|
+
/** @type {Headers} */
|
|
23570
|
+
var requestHeaders = {};
|
|
23571
|
+
req.headers.forEach(function(value, header) {
|
|
23572
|
+
if (shouldRecordHeader('request', options.recordHeaders, header)) {
|
|
23573
|
+
requestHeaders[header] = value;
|
|
23574
|
+
}
|
|
23575
|
+
});
|
|
23576
|
+
networkRequest.requestHeaders = requestHeaders;
|
|
23577
|
+
|
|
23578
|
+
if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
|
|
23579
|
+
requestBodyPromise = tryReadFetchBody(req)
|
|
23580
|
+
.then(function(body) {
|
|
23581
|
+
networkRequest.requestBody = body;
|
|
23582
|
+
});
|
|
23583
|
+
}
|
|
23584
|
+
|
|
23585
|
+
after = win.performance.now();
|
|
23586
|
+
originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
|
|
23587
|
+
res = response;
|
|
23588
|
+
before = win.performance.now();
|
|
23589
|
+
|
|
23590
|
+
/** @type {Headers} */
|
|
23591
|
+
var responseHeaders = {};
|
|
23592
|
+
res.headers.forEach(function(value, header) {
|
|
23593
|
+
if (shouldRecordHeader('response', options.recordHeaders, header)) {
|
|
23594
|
+
responseHeaders[header] = value;
|
|
23595
|
+
}
|
|
23596
|
+
});
|
|
23597
|
+
networkRequest.responseHeaders = responseHeaders;
|
|
23598
|
+
|
|
23599
|
+
if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
|
|
23600
|
+
responseBodyPromise = tryReadFetchBody(res)
|
|
23601
|
+
.then(function(body) {
|
|
23602
|
+
networkRequest.responseBody = body;
|
|
23603
|
+
});
|
|
23604
|
+
}
|
|
23605
|
+
|
|
23606
|
+
return res;
|
|
23607
|
+
});
|
|
23608
|
+
} catch (e) {
|
|
23609
|
+
originalFetchPromise = Promise.reject(e);
|
|
23610
|
+
}
|
|
23611
|
+
|
|
23612
|
+
// await concurrently so we don't delay the fetch response
|
|
23613
|
+
Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
|
|
23614
|
+
.then(function () {
|
|
23615
|
+
return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
|
|
23616
|
+
})
|
|
23617
|
+
.then(function(entry) {
|
|
23618
|
+
if (!entry) {
|
|
23619
|
+
logger$3.error('Failed to get performance entry for fetch request to ' + req.url);
|
|
23620
|
+
return;
|
|
23621
|
+
}
|
|
23622
|
+
/** @type {NetworkRequest} */
|
|
23623
|
+
var request = {
|
|
23624
|
+
url: entry.name,
|
|
23625
|
+
method: req.method,
|
|
23626
|
+
initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
|
|
23627
|
+
status: res ? res.status : undefined,
|
|
23628
|
+
startTime: Math.round(entry.startTime),
|
|
23629
|
+
endTime: Math.round(entry.responseEnd),
|
|
23630
|
+
timeOrigin: getTimeOrigin(win),
|
|
23631
|
+
requestHeaders: networkRequest.requestHeaders,
|
|
23632
|
+
requestBody: networkRequest.requestBody,
|
|
23633
|
+
responseHeaders: networkRequest.responseHeaders,
|
|
23634
|
+
responseBody: networkRequest.responseBody,
|
|
23635
|
+
};
|
|
23636
|
+
cb({ requests: [request] });
|
|
23637
|
+
})
|
|
23638
|
+
.catch(function (e) {
|
|
23639
|
+
logger$3.error('Error recording fetch request to ' + req.url + ': ' + String(e));
|
|
23640
|
+
});
|
|
23641
|
+
|
|
23642
|
+
return originalFetchPromise;
|
|
23643
|
+
};
|
|
23644
|
+
});
|
|
23645
|
+
return function() {
|
|
23646
|
+
restorePatch();
|
|
23647
|
+
};
|
|
23648
|
+
}
|
|
23649
|
+
|
|
23650
|
+
/**
|
|
23651
|
+
* @param {networkCallback} callback
|
|
23652
|
+
* @param {Window} win
|
|
23653
|
+
* @param {NetworkRecordOptions} options
|
|
23654
|
+
* @returns {listenerHandler}
|
|
23655
|
+
*/
|
|
23656
|
+
function initNetworkObserver(callback, win, options) {
|
|
23657
|
+
if (!('performance' in win)) {
|
|
23658
|
+
return function() {
|
|
23659
|
+
//
|
|
23660
|
+
};
|
|
23661
|
+
}
|
|
23662
|
+
|
|
23663
|
+
var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
|
|
23664
|
+
var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
|
|
23665
|
+
options = Object.assign({}, options, {
|
|
23666
|
+
recordHeaders: recordHeaders,
|
|
23667
|
+
recordBodyUrls: recordBodyUrls,
|
|
23668
|
+
});
|
|
23669
|
+
var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
|
|
23670
|
+
|
|
23671
|
+
/** @type {networkCallback} */
|
|
23672
|
+
var cb = function(data) {
|
|
23673
|
+
var requests = data.requests.filter(function(request) {
|
|
23674
|
+
var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
|
|
23675
|
+
return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
|
|
23676
|
+
});
|
|
23677
|
+
if (requests.length > 0 || data.isInitial) {
|
|
23678
|
+
callback(Object.assign({}, data, { requests: requests }));
|
|
23679
|
+
}
|
|
23680
|
+
};
|
|
23681
|
+
var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
|
|
23682
|
+
var xhrObserver = initXhrObserver(cb, win, networkOptions);
|
|
23683
|
+
var fetchObserver = initFetchObserver(cb, win, networkOptions);
|
|
23684
|
+
return function() {
|
|
23685
|
+
performanceObserver();
|
|
23686
|
+
xhrObserver();
|
|
23687
|
+
fetchObserver();
|
|
23688
|
+
};
|
|
23689
|
+
}
|
|
23690
|
+
|
|
23691
|
+
// arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
|
|
23692
|
+
// a changed format in the mixpanel product.
|
|
23693
|
+
var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
|
|
23694
|
+
|
|
23695
|
+
/**
|
|
23696
|
+
* @param {NetworkRecordOptions} [options]
|
|
23697
|
+
* @returns {RecordPlugin}
|
|
23698
|
+
*/
|
|
23699
|
+
var getRecordNetworkPlugin = function(options) {
|
|
23700
|
+
return {
|
|
23701
|
+
name: NETWORK_PLUGIN_NAME,
|
|
23702
|
+
observer: initNetworkObserver,
|
|
23703
|
+
options: options,
|
|
23704
|
+
};
|
|
23705
|
+
};
|
|
23706
|
+
|
|
23707
|
+
/**
|
|
23708
|
+
* @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
|
|
23709
|
+
*/
|
|
23710
|
+
|
|
23711
|
+
|
|
23712
|
+
var logger$2 = console_with_prefix('recorder');
|
|
23713
|
+
var CompressionStream = win['CompressionStream'];
|
|
23714
|
+
|
|
23715
|
+
var RECORDER_BATCHER_LIB_CONFIG = {
|
|
23716
|
+
'batch_size': 1000,
|
|
23717
|
+
'batch_flush_interval_ms': 10 * 1000,
|
|
23718
|
+
'batch_request_timeout_ms': 90 * 1000,
|
|
23719
|
+
'batch_autostart': true
|
|
23720
|
+
};
|
|
23721
|
+
|
|
23722
|
+
var ACTIVE_SOURCES = new Set([
|
|
23723
|
+
IncrementalSource.MouseMove,
|
|
23724
|
+
IncrementalSource.MouseInteraction,
|
|
23725
|
+
IncrementalSource.Scroll,
|
|
23726
|
+
IncrementalSource.ViewportResize,
|
|
23727
|
+
IncrementalSource.Input,
|
|
23728
|
+
IncrementalSource.TouchMove,
|
|
23729
|
+
IncrementalSource.MediaInteraction,
|
|
23730
|
+
IncrementalSource.Drag,
|
|
23731
|
+
IncrementalSource.Selection,
|
|
23732
|
+
]);
|
|
23733
|
+
|
|
23734
|
+
function isUserEvent(ev) {
|
|
23735
|
+
return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
|
|
23736
|
+
}
|
|
23737
|
+
|
|
23738
|
+
/**
|
|
23739
|
+
* @typedef {Object} SerializedRecording
|
|
23740
|
+
* @property {number} idleExpires
|
|
23741
|
+
* @property {number} maxExpires
|
|
23742
|
+
* @property {number} replayStartTime
|
|
23743
|
+
* @property {number} lastEventTimestamp
|
|
23744
|
+
* @property {number} seqNo
|
|
23745
|
+
* @property {string} batchStartUrl
|
|
23746
|
+
* @property {string} replayId
|
|
23747
|
+
* @property {string} tabId
|
|
23748
|
+
* @property {string} replayStartUrl
|
|
23749
|
+
*/
|
|
23750
|
+
|
|
23751
|
+
/**
|
|
23752
|
+
* @typedef {Object} SessionRecordingOptions
|
|
23753
|
+
* @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
|
|
23754
|
+
* @property {String} [options.replayId] - unique uuid for a single replay
|
|
23755
|
+
* @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
|
|
23756
|
+
* @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
|
|
23757
|
+
* @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
|
|
23758
|
+
* @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
|
|
23759
|
+
* @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
|
|
23760
|
+
* optional properties for deserialization:
|
|
23761
|
+
* @property {number} idleExpires
|
|
23762
|
+
* @property {number} maxExpires
|
|
23763
|
+
* @property {number} replayStartTime
|
|
23764
|
+
* @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
|
|
23765
|
+
* @property {number} seqNo
|
|
23766
|
+
* @property {string} batchStartUrl
|
|
23767
|
+
* @property {string} replayStartUrl
|
|
23768
|
+
*/
|
|
23769
|
+
|
|
23770
|
+
/**
|
|
23771
|
+
* @typedef {Object} UserIdInfo
|
|
23772
|
+
* @property {string} distinct_id
|
|
23773
|
+
* @property {string} user_id
|
|
23774
|
+
* @property {string} device_id
|
|
23775
|
+
*/
|
|
23776
|
+
|
|
23777
|
+
|
|
23778
|
+
/**
|
|
23779
|
+
* This class encapsulates a single session recording and its lifecycle.
|
|
23780
|
+
* @param {SessionRecordingOptions} options
|
|
23781
|
+
*/
|
|
23782
|
+
var SessionRecording = function(options) {
|
|
23783
|
+
this._mixpanel = options.mixpanelInstance;
|
|
23784
|
+
this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
|
|
23785
|
+
this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
|
|
23786
|
+
this._onBatchSent = options.onBatchSent || NOOP_FUNC;
|
|
23787
|
+
this._rrwebRecord = options.rrwebRecord || null;
|
|
23788
|
+
|
|
23789
|
+
// internal rrweb stopRecording function
|
|
23790
|
+
this._stopRecording = null;
|
|
23791
|
+
this.replayId = options.replayId;
|
|
23792
|
+
|
|
23793
|
+
this.batchStartUrl = options.batchStartUrl || null;
|
|
23794
|
+
this.replayStartUrl = options.replayStartUrl || null;
|
|
23795
|
+
this.idleExpires = options.idleExpires || null;
|
|
23796
|
+
this.maxExpires = options.maxExpires || null;
|
|
23797
|
+
this.replayStartTime = options.replayStartTime || null;
|
|
23798
|
+
this.lastEventTimestamp = options.lastEventTimestamp || null;
|
|
23799
|
+
this.seqNo = options.seqNo || 0;
|
|
23800
|
+
|
|
23801
|
+
this.idleTimeoutId = null;
|
|
23802
|
+
this.maxTimeoutId = null;
|
|
23803
|
+
|
|
23804
|
+
this.recordMaxMs = MAX_RECORDING_MS;
|
|
23805
|
+
this.recordMinMs = 0;
|
|
23806
|
+
|
|
23807
|
+
// disable persistence if localStorage is not supported
|
|
23808
|
+
// request-queue will automatically disable persistence if indexedDB fails to initialize
|
|
23809
|
+
var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
|
|
23810
|
+
|
|
23811
|
+
// each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
|
|
23812
|
+
this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
|
|
23813
|
+
this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
|
|
23814
|
+
this.batcher = new RequestBatcher(this.batcherKey, {
|
|
23815
|
+
errorReporter: this.reportError.bind(this),
|
|
23816
|
+
flushOnlyOnInterval: true,
|
|
23817
|
+
libConfig: RECORDER_BATCHER_LIB_CONFIG,
|
|
23818
|
+
sendRequestFunc: this.flushEventsWithOptOut.bind(this),
|
|
23819
|
+
queueStorage: this.queueStorage,
|
|
23820
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
23821
|
+
usePersistence: usePersistence,
|
|
23822
|
+
stopAllBatchingFunc: this.stopRecording.bind(this),
|
|
23823
|
+
|
|
23824
|
+
// increased throttle and shared lock timeout because recording events are very high frequency.
|
|
23825
|
+
// this will minimize the amount of lock contention between enqueued events.
|
|
23826
|
+
// for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
|
|
23827
|
+
enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
|
|
23828
|
+
sharedLockTimeoutMS: 10 * 1000,
|
|
23829
|
+
});
|
|
23830
|
+
};
|
|
23831
|
+
|
|
23832
|
+
/**
|
|
23833
|
+
* @returns {UserIdInfo}
|
|
23834
|
+
*/
|
|
23835
|
+
SessionRecording.prototype.getUserIdInfo = function () {
|
|
23836
|
+
if (this.finalFlushUserIdInfo) {
|
|
23837
|
+
return this.finalFlushUserIdInfo;
|
|
23838
|
+
}
|
|
23839
|
+
|
|
23840
|
+
var userIdInfo = {
|
|
23841
|
+
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
23842
|
+
};
|
|
23843
|
+
|
|
23844
|
+
// send ID management props if they exist
|
|
23845
|
+
var deviceId = this._mixpanel.get_property('$device_id');
|
|
23846
|
+
if (deviceId) {
|
|
23847
|
+
userIdInfo['$device_id'] = deviceId;
|
|
23848
|
+
}
|
|
23849
|
+
var userId = this._mixpanel.get_property('$user_id');
|
|
23850
|
+
if (userId) {
|
|
23851
|
+
userIdInfo['$user_id'] = userId;
|
|
23852
|
+
}
|
|
23853
|
+
return userIdInfo;
|
|
23854
|
+
};
|
|
23855
|
+
|
|
23856
|
+
SessionRecording.prototype.unloadPersistedData = function () {
|
|
23857
|
+
this.batcher.stop();
|
|
23858
|
+
|
|
23859
|
+
return this.queueStorage.init().catch(function () {
|
|
23860
|
+
this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
|
|
23861
|
+
}.bind(this)).then(function () {
|
|
23862
|
+
// if the recording is too short, just delete any stored events without flushing
|
|
23863
|
+
if (this.getDurationMs() < this._getRecordMinMs()) {
|
|
23864
|
+
return this.queueStorage.removeItem(this.batcherKey);
|
|
23865
|
+
}
|
|
23866
|
+
|
|
23867
|
+
return this.batcher.flush()
|
|
23868
|
+
.then(function () {
|
|
23869
|
+
return this.queueStorage.removeItem(this.batcherKey);
|
|
23870
|
+
}.bind(this));
|
|
23871
|
+
}.bind(this));
|
|
23872
|
+
};
|
|
23873
|
+
|
|
23874
|
+
SessionRecording.prototype.getConfig = function(configVar) {
|
|
23103
23875
|
return this._mixpanel.get_config(configVar);
|
|
23104
23876
|
};
|
|
23105
23877
|
|
|
@@ -23165,6 +23937,29 @@
|
|
|
23165
23937
|
|
|
23166
23938
|
var privacyConfig = getPrivacyConfig(this._mixpanel);
|
|
23167
23939
|
|
|
23940
|
+
var plugins = [];
|
|
23941
|
+
if (this.getConfig('record_network')) {
|
|
23942
|
+
var options = this.getConfig('record_network_options') || {};
|
|
23943
|
+
// don't track requests to Mixpanel /record API
|
|
23944
|
+
var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
|
|
23945
|
+
ignoreRequestUrls.push(this._getApiRoute());
|
|
23946
|
+
options.ignoreRequestUrls = ignoreRequestUrls;
|
|
23947
|
+
|
|
23948
|
+
plugins.push(getRecordNetworkPlugin(options));
|
|
23949
|
+
}
|
|
23950
|
+
|
|
23951
|
+
if (this.getConfig('record_console')) {
|
|
23952
|
+
plugins.push(
|
|
23953
|
+
getRecordConsolePlugin({
|
|
23954
|
+
stringifyOptions: {
|
|
23955
|
+
stringLengthLimit: 1000,
|
|
23956
|
+
numOfKeysLimit: 50,
|
|
23957
|
+
depthOfLimit: 2
|
|
23958
|
+
}
|
|
23959
|
+
})
|
|
23960
|
+
);
|
|
23961
|
+
}
|
|
23962
|
+
|
|
23168
23963
|
try {
|
|
23169
23964
|
this._stopRecording = this._rrwebRecord({
|
|
23170
23965
|
'emit': function (ev) {
|
|
@@ -23203,15 +23998,7 @@
|
|
|
23203
23998
|
'sampling': {
|
|
23204
23999
|
'canvas': 15
|
|
23205
24000
|
},
|
|
23206
|
-
'plugins':
|
|
23207
|
-
getRecordConsolePlugin({
|
|
23208
|
-
stringifyOptions: {
|
|
23209
|
-
stringLengthLimit: 1000,
|
|
23210
|
-
numOfKeysLimit: 50,
|
|
23211
|
-
depthOfLimit: 2
|
|
23212
|
-
}
|
|
23213
|
-
})
|
|
23214
|
-
] : []
|
|
24001
|
+
'plugins': plugins,
|
|
23215
24002
|
});
|
|
23216
24003
|
} catch (err) {
|
|
23217
24004
|
this.reportError('Unexpected error when starting rrweb recording.', err);
|
|
@@ -23326,6 +24113,10 @@
|
|
|
23326
24113
|
return recording;
|
|
23327
24114
|
};
|
|
23328
24115
|
|
|
24116
|
+
SessionRecording.prototype._getApiRoute = function () {
|
|
24117
|
+
return this.getConfig('api_routes')['record'];
|
|
24118
|
+
};
|
|
24119
|
+
|
|
23329
24120
|
SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
|
|
23330
24121
|
var onSuccess = function (response, responseBody) {
|
|
23331
24122
|
// Update batch specific props only if the request was successful to guarantee ordering.
|
|
@@ -23345,7 +24136,7 @@
|
|
|
23345
24136
|
});
|
|
23346
24137
|
}.bind(this);
|
|
23347
24138
|
var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
|
|
23348
|
-
win['fetch'](apiHost + '/' + this.
|
|
24139
|
+
win['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
|
|
23349
24140
|
'method': 'POST',
|
|
23350
24141
|
'headers': {
|
|
23351
24142
|
'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
|
|
@@ -23746,8 +24537,12 @@
|
|
|
23746
24537
|
this.startRecording({shouldStopBatcher: true});
|
|
23747
24538
|
};
|
|
23748
24539
|
|
|
24540
|
+
MixpanelRecorder.prototype.isRecording = function () {
|
|
24541
|
+
return this.activeRecording && !this.activeRecording.isRrwebStopped();
|
|
24542
|
+
};
|
|
24543
|
+
|
|
23749
24544
|
MixpanelRecorder.prototype.getActiveReplayId = function () {
|
|
23750
|
-
if (this.
|
|
24545
|
+
if (this.isRecording()) {
|
|
23751
24546
|
return this.activeRecording.replayId;
|
|
23752
24547
|
} else {
|
|
23753
24548
|
return null;
|
|
@@ -23762,7 +24557,7 @@
|
|
|
23762
24557
|
}
|
|
23763
24558
|
});
|
|
23764
24559
|
|
|
23765
|
-
win[
|
|
24560
|
+
win[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
|
|
23766
24561
|
|
|
23767
24562
|
/** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
|
|
23768
24563
|
/** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
|
|
@@ -23864,7 +24659,7 @@
|
|
|
23864
24659
|
observer.observe(shadowRoot, this.observerConfig);
|
|
23865
24660
|
this.shadowObservers.push(observer);
|
|
23866
24661
|
} catch (e) {
|
|
23867
|
-
logger$
|
|
24662
|
+
logger$4.critical('Error while observing shadow root', e);
|
|
23868
24663
|
}
|
|
23869
24664
|
};
|
|
23870
24665
|
|
|
@@ -23875,7 +24670,7 @@
|
|
|
23875
24670
|
}
|
|
23876
24671
|
|
|
23877
24672
|
if (!weakSetSupported()) {
|
|
23878
|
-
logger$
|
|
24673
|
+
logger$4.critical('Shadow DOM observation unavailable: WeakSet not supported');
|
|
23879
24674
|
return;
|
|
23880
24675
|
}
|
|
23881
24676
|
|
|
@@ -23891,7 +24686,7 @@
|
|
|
23891
24686
|
try {
|
|
23892
24687
|
this.shadowObservers[i].disconnect();
|
|
23893
24688
|
} catch (e) {
|
|
23894
|
-
logger$
|
|
24689
|
+
logger$4.critical('Error while disconnecting shadow DOM observer', e);
|
|
23895
24690
|
}
|
|
23896
24691
|
}
|
|
23897
24692
|
this.shadowObservers = [];
|
|
@@ -24079,7 +24874,7 @@
|
|
|
24079
24874
|
|
|
24080
24875
|
this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
|
|
24081
24876
|
} catch (e) {
|
|
24082
|
-
logger$
|
|
24877
|
+
logger$4.critical('Error while setting up mutation observer', e);
|
|
24083
24878
|
}
|
|
24084
24879
|
}
|
|
24085
24880
|
|
|
@@ -24094,7 +24889,7 @@
|
|
|
24094
24889
|
);
|
|
24095
24890
|
this.shadowDOMObserver.start();
|
|
24096
24891
|
} catch (e) {
|
|
24097
|
-
logger$
|
|
24892
|
+
logger$4.critical('Error while setting up shadow DOM observer', e);
|
|
24098
24893
|
this.shadowDOMObserver = null;
|
|
24099
24894
|
}
|
|
24100
24895
|
}
|
|
@@ -24121,7 +24916,7 @@
|
|
|
24121
24916
|
try {
|
|
24122
24917
|
listener.target.removeEventListener(listener.event, listener.handler, listener.options);
|
|
24123
24918
|
} catch (e) {
|
|
24124
|
-
logger$
|
|
24919
|
+
logger$4.critical('Error while removing event listener', e);
|
|
24125
24920
|
}
|
|
24126
24921
|
}
|
|
24127
24922
|
this.eventListeners = [];
|
|
@@ -24130,7 +24925,7 @@
|
|
|
24130
24925
|
try {
|
|
24131
24926
|
this.mutationObserver.disconnect();
|
|
24132
24927
|
} catch (e) {
|
|
24133
|
-
logger$
|
|
24928
|
+
logger$4.critical('Error while disconnecting mutation observer', e);
|
|
24134
24929
|
}
|
|
24135
24930
|
this.mutationObserver = null;
|
|
24136
24931
|
}
|
|
@@ -24139,7 +24934,7 @@
|
|
|
24139
24934
|
try {
|
|
24140
24935
|
this.shadowDOMObserver.stop();
|
|
24141
24936
|
} catch (e) {
|
|
24142
|
-
logger$
|
|
24937
|
+
logger$4.critical('Error while stopping shadow DOM observer', e);
|
|
24143
24938
|
}
|
|
24144
24939
|
this.shadowDOMObserver = null;
|
|
24145
24940
|
}
|
|
@@ -24217,7 +25012,7 @@
|
|
|
24217
25012
|
|
|
24218
25013
|
Autocapture.prototype.init = function() {
|
|
24219
25014
|
if (!minDOMApisSupported()) {
|
|
24220
|
-
logger$
|
|
25015
|
+
logger$4.critical('Autocapture unavailable: missing required DOM APIs');
|
|
24221
25016
|
return;
|
|
24222
25017
|
}
|
|
24223
25018
|
this.initPageListeners();
|
|
@@ -24249,27 +25044,15 @@
|
|
|
24249
25044
|
};
|
|
24250
25045
|
|
|
24251
25046
|
Autocapture.prototype.currentUrlBlocked = function() {
|
|
24252
|
-
var i;
|
|
24253
25047
|
var currentUrl = _.info.currentUrl();
|
|
24254
25048
|
|
|
24255
25049
|
var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
|
|
24256
25050
|
if (allowUrlRegexes.length) {
|
|
24257
25051
|
// we're using an allowlist, only track if current URL matches
|
|
24258
|
-
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
if (currentUrl.match(allowRegex)) {
|
|
24263
|
-
allowed = true;
|
|
24264
|
-
break;
|
|
24265
|
-
}
|
|
24266
|
-
} catch (err) {
|
|
24267
|
-
logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
|
|
24268
|
-
return true;
|
|
24269
|
-
}
|
|
24270
|
-
}
|
|
24271
|
-
if (!allowed) {
|
|
24272
|
-
// wasn't allowed by any regex
|
|
25052
|
+
try {
|
|
25053
|
+
return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
|
|
25054
|
+
} catch (err) {
|
|
25055
|
+
logger$4.critical('Error while checking block URL regexes: ', err);
|
|
24273
25056
|
return true;
|
|
24274
25057
|
}
|
|
24275
25058
|
}
|
|
@@ -24279,17 +25062,12 @@
|
|
|
24279
25062
|
return false;
|
|
24280
25063
|
}
|
|
24281
25064
|
|
|
24282
|
-
|
|
24283
|
-
|
|
24284
|
-
|
|
24285
|
-
|
|
24286
|
-
|
|
24287
|
-
} catch (err) {
|
|
24288
|
-
logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
|
|
24289
|
-
return true;
|
|
24290
|
-
}
|
|
25065
|
+
try {
|
|
25066
|
+
return urlMatchesRegexList(currentUrl, blockUrlRegexes);
|
|
25067
|
+
} catch (err) {
|
|
25068
|
+
logger$4.critical('Error while checking block URL regexes: ', err);
|
|
25069
|
+
return true;
|
|
24291
25070
|
}
|
|
24292
|
-
return false;
|
|
24293
25071
|
};
|
|
24294
25072
|
|
|
24295
25073
|
Autocapture.prototype.pageviewTrackingConfig = function() {
|
|
@@ -24425,7 +25203,7 @@
|
|
|
24425
25203
|
return;
|
|
24426
25204
|
}
|
|
24427
25205
|
|
|
24428
|
-
logger$
|
|
25206
|
+
logger$4.log('Initializing scroll depth tracking');
|
|
24429
25207
|
|
|
24430
25208
|
this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
|
|
24431
25209
|
|
|
@@ -24451,7 +25229,7 @@
|
|
|
24451
25229
|
if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
|
|
24452
25230
|
return;
|
|
24453
25231
|
}
|
|
24454
|
-
logger$
|
|
25232
|
+
logger$4.log('Initializing click tracking');
|
|
24455
25233
|
|
|
24456
25234
|
this.listenerClick = function(ev) {
|
|
24457
25235
|
if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
|
|
@@ -24470,7 +25248,7 @@
|
|
|
24470
25248
|
return;
|
|
24471
25249
|
}
|
|
24472
25250
|
|
|
24473
|
-
logger$
|
|
25251
|
+
logger$4.log('Initializing dead click tracking');
|
|
24474
25252
|
if (!this._deadClickTracker) {
|
|
24475
25253
|
this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
|
|
24476
25254
|
this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
|
|
@@ -24504,7 +25282,7 @@
|
|
|
24504
25282
|
if (!this.getConfig(CONFIG_TRACK_INPUT)) {
|
|
24505
25283
|
return;
|
|
24506
25284
|
}
|
|
24507
|
-
logger$
|
|
25285
|
+
logger$4.log('Initializing input tracking');
|
|
24508
25286
|
|
|
24509
25287
|
this.listenerChange = function(ev) {
|
|
24510
25288
|
if (!this.getConfig(CONFIG_TRACK_INPUT)) {
|
|
@@ -24521,7 +25299,7 @@
|
|
|
24521
25299
|
if (!this.pageviewTrackingConfig()) {
|
|
24522
25300
|
return;
|
|
24523
25301
|
}
|
|
24524
|
-
logger$
|
|
25302
|
+
logger$4.log('Initializing pageview tracking');
|
|
24525
25303
|
|
|
24526
25304
|
var previousTrackedUrl = '';
|
|
24527
25305
|
var tracked = false;
|
|
@@ -24556,7 +25334,7 @@
|
|
|
24556
25334
|
}
|
|
24557
25335
|
if (didPathChange) {
|
|
24558
25336
|
this.lastScrollCheckpoint = 0;
|
|
24559
|
-
logger$
|
|
25337
|
+
logger$4.log('Path change: re-initializing scroll depth checkpoints');
|
|
24560
25338
|
}
|
|
24561
25339
|
}
|
|
24562
25340
|
}.bind(this));
|
|
@@ -24571,7 +25349,7 @@
|
|
|
24571
25349
|
return;
|
|
24572
25350
|
}
|
|
24573
25351
|
|
|
24574
|
-
logger$
|
|
25352
|
+
logger$4.log('Initializing rage click tracking');
|
|
24575
25353
|
if (!this._rageClickTracker) {
|
|
24576
25354
|
this._rageClickTracker = new RageClickTracker();
|
|
24577
25355
|
}
|
|
@@ -24601,7 +25379,7 @@
|
|
|
24601
25379
|
if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
|
|
24602
25380
|
return;
|
|
24603
25381
|
}
|
|
24604
|
-
logger$
|
|
25382
|
+
logger$4.log('Initializing scroll tracking');
|
|
24605
25383
|
this.lastScrollCheckpoint = 0;
|
|
24606
25384
|
|
|
24607
25385
|
var scrollTrackFunction = function() {
|
|
@@ -24638,7 +25416,7 @@
|
|
|
24638
25416
|
}
|
|
24639
25417
|
}
|
|
24640
25418
|
} catch (err) {
|
|
24641
|
-
logger$
|
|
25419
|
+
logger$4.critical('Error while calculating scroll percentage', err);
|
|
24642
25420
|
}
|
|
24643
25421
|
if (shouldTrack) {
|
|
24644
25422
|
this.mp.track(MP_EV_SCROLL, props);
|
|
@@ -24656,7 +25434,7 @@
|
|
|
24656
25434
|
if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
|
|
24657
25435
|
return;
|
|
24658
25436
|
}
|
|
24659
|
-
logger$
|
|
25437
|
+
logger$4.log('Initializing submit tracking');
|
|
24660
25438
|
|
|
24661
25439
|
this.listenerSubmit = function(ev) {
|
|
24662
25440
|
if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
|
|
@@ -24678,7 +25456,7 @@
|
|
|
24678
25456
|
return;
|
|
24679
25457
|
}
|
|
24680
25458
|
|
|
24681
|
-
logger$
|
|
25459
|
+
logger$4.log('Initializing page visibility tracking.');
|
|
24682
25460
|
this._initScrollDepthTracking();
|
|
24683
25461
|
var previousTrackedUrl = _.info.currentUrl();
|
|
24684
25462
|
|
|
@@ -24733,14 +25511,62 @@
|
|
|
24733
25511
|
// TODO integrate error_reporter from mixpanel instance
|
|
24734
25512
|
safewrapClass(Autocapture);
|
|
24735
25513
|
|
|
24736
|
-
|
|
25514
|
+
/**
|
|
25515
|
+
* Get the promise-based targeting loader
|
|
25516
|
+
* @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
|
|
25517
|
+
* @param {string} targetingSrc - URL to targeting bundle
|
|
25518
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
25519
|
+
*/
|
|
25520
|
+
var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
|
|
25521
|
+
// Return existing promise if already initialized or loading
|
|
25522
|
+
if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
|
|
25523
|
+
return win[TARGETING_GLOBAL_NAME];
|
|
25524
|
+
}
|
|
25525
|
+
|
|
25526
|
+
// Create loading promise and set it as the global immediately
|
|
25527
|
+
// This makes minified build behavior consistent with dev/CJS builds
|
|
25528
|
+
win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
|
|
25529
|
+
loadExtraBundle(targetingSrc, resolve);
|
|
25530
|
+
}).then(function () {
|
|
25531
|
+
var p = win[TARGETING_GLOBAL_NAME];
|
|
25532
|
+
if (p && typeof p.then === 'function') {
|
|
25533
|
+
return p;
|
|
25534
|
+
}
|
|
25535
|
+
throw new Error('targeting failed to load');
|
|
25536
|
+
}).catch(function (err) {
|
|
25537
|
+
delete win[TARGETING_GLOBAL_NAME];
|
|
25538
|
+
throw err;
|
|
25539
|
+
});
|
|
25540
|
+
|
|
25541
|
+
return win[TARGETING_GLOBAL_NAME];
|
|
25542
|
+
};
|
|
24737
25543
|
|
|
25544
|
+
var logger = console_with_prefix('flags');
|
|
24738
25545
|
var FLAGS_CONFIG_KEY = 'flags';
|
|
24739
25546
|
|
|
24740
25547
|
var CONFIG_CONTEXT = 'context';
|
|
24741
25548
|
var CONFIG_DEFAULTS = {};
|
|
24742
25549
|
CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
|
|
24743
25550
|
|
|
25551
|
+
/**
|
|
25552
|
+
* Generate a unique key for a pending first-time event
|
|
25553
|
+
* @param {string} flagKey - The flag key
|
|
25554
|
+
* @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
|
|
25555
|
+
* @returns {string} Composite key in format "flagKey:firstTimeEventHash"
|
|
25556
|
+
*/
|
|
25557
|
+
var getPendingEventKey = function(flagKey, firstTimeEventHash) {
|
|
25558
|
+
return flagKey + ':' + firstTimeEventHash;
|
|
25559
|
+
};
|
|
25560
|
+
|
|
25561
|
+
/**
|
|
25562
|
+
* Extract the flag key from a pending event key
|
|
25563
|
+
* @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
|
|
25564
|
+
* @returns {string} The flag key portion
|
|
25565
|
+
*/
|
|
25566
|
+
var getFlagKeyFromPendingEventKey = function(eventKey) {
|
|
25567
|
+
return eventKey.split(':')[0];
|
|
25568
|
+
};
|
|
25569
|
+
|
|
24744
25570
|
/**
|
|
24745
25571
|
* FeatureFlagManager: support for Mixpanel's feature flagging product
|
|
24746
25572
|
* @constructor
|
|
@@ -24752,6 +25578,8 @@
|
|
|
24752
25578
|
this.setMpConfig = initOptions.setConfigFunc;
|
|
24753
25579
|
this.getMpProperty = initOptions.getPropertyFunc;
|
|
24754
25580
|
this.track = initOptions.trackingFunc;
|
|
25581
|
+
this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
|
|
25582
|
+
this.targetingSrc = initOptions.targetingSrc || '';
|
|
24755
25583
|
};
|
|
24756
25584
|
|
|
24757
25585
|
FeatureFlagManager.prototype.init = function() {
|
|
@@ -24764,6 +25592,8 @@
|
|
|
24764
25592
|
this.fetchFlags();
|
|
24765
25593
|
|
|
24766
25594
|
this.trackedFeatures = new Set();
|
|
25595
|
+
this.pendingFirstTimeEvents = {};
|
|
25596
|
+
this.activatedFirstTimeEvents = {};
|
|
24767
25597
|
};
|
|
24768
25598
|
|
|
24769
25599
|
FeatureFlagManager.prototype.getFullConfig = function() {
|
|
@@ -24844,17 +25674,78 @@
|
|
|
24844
25674
|
throw new Error('No flags in API response');
|
|
24845
25675
|
}
|
|
24846
25676
|
var flags = new Map();
|
|
25677
|
+
var pendingFirstTimeEvents = {};
|
|
25678
|
+
|
|
25679
|
+
// Process flags from response
|
|
24847
25680
|
_.each(responseFlags, function(data, key) {
|
|
24848
|
-
|
|
24849
|
-
|
|
24850
|
-
|
|
24851
|
-
|
|
24852
|
-
|
|
24853
|
-
|
|
25681
|
+
// Check if this flag has any activated first-time events this session
|
|
25682
|
+
var hasActivatedEvent = false;
|
|
25683
|
+
var prefix = key + ':';
|
|
25684
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
25685
|
+
if (eventKey.startsWith(prefix)) {
|
|
25686
|
+
hasActivatedEvent = true;
|
|
25687
|
+
}
|
|
24854
25688
|
});
|
|
24855
|
-
|
|
25689
|
+
|
|
25690
|
+
if (hasActivatedEvent) {
|
|
25691
|
+
// Preserve the activated variant, don't overwrite with server's current variant
|
|
25692
|
+
var currentFlag = this.flags && this.flags.get(key);
|
|
25693
|
+
if (currentFlag) {
|
|
25694
|
+
flags.set(key, currentFlag);
|
|
25695
|
+
}
|
|
25696
|
+
} else {
|
|
25697
|
+
// Use server's current variant
|
|
25698
|
+
flags.set(key, {
|
|
25699
|
+
'key': data['variant_key'],
|
|
25700
|
+
'value': data['variant_value'],
|
|
25701
|
+
'experiment_id': data['experiment_id'],
|
|
25702
|
+
'is_experiment_active': data['is_experiment_active'],
|
|
25703
|
+
'is_qa_tester': data['is_qa_tester']
|
|
25704
|
+
});
|
|
25705
|
+
}
|
|
25706
|
+
}, this);
|
|
25707
|
+
|
|
25708
|
+
// Process top-level pending_first_time_events array
|
|
25709
|
+
var topLevelDefinitions = responseBody['pending_first_time_events'];
|
|
25710
|
+
if (topLevelDefinitions && topLevelDefinitions.length > 0) {
|
|
25711
|
+
_.each(topLevelDefinitions, function(def) {
|
|
25712
|
+
var flagKey = def['flag_key'];
|
|
25713
|
+
var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
|
|
25714
|
+
|
|
25715
|
+
// Skip if this specific event has already been activated this session
|
|
25716
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
25717
|
+
return;
|
|
25718
|
+
}
|
|
25719
|
+
|
|
25720
|
+
// Store pending event definition using composite key
|
|
25721
|
+
pendingFirstTimeEvents[eventKey] = {
|
|
25722
|
+
'flag_key': flagKey,
|
|
25723
|
+
'flag_id': def['flag_id'],
|
|
25724
|
+
'project_id': def['project_id'],
|
|
25725
|
+
'first_time_event_hash': def['first_time_event_hash'],
|
|
25726
|
+
'event_name': def['event_name'],
|
|
25727
|
+
'property_filters': def['property_filters'],
|
|
25728
|
+
'pending_variant': def['pending_variant']
|
|
25729
|
+
};
|
|
25730
|
+
}, this);
|
|
25731
|
+
}
|
|
25732
|
+
|
|
25733
|
+
// Preserve any activated orphaned flags (flags that were activated but are no longer in response)
|
|
25734
|
+
if (this.activatedFirstTimeEvents) {
|
|
25735
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
25736
|
+
var flagKey = getFlagKeyFromPendingEventKey(eventKey);
|
|
25737
|
+
if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
|
|
25738
|
+
// Keep the activated flag even though it's not in the new response
|
|
25739
|
+
flags.set(flagKey, this.flags.get(flagKey));
|
|
25740
|
+
}
|
|
25741
|
+
}, this);
|
|
25742
|
+
}
|
|
25743
|
+
|
|
24856
25744
|
this.flags = flags;
|
|
25745
|
+
this.pendingFirstTimeEvents = pendingFirstTimeEvents;
|
|
24857
25746
|
this._traceparent = traceparent;
|
|
25747
|
+
|
|
25748
|
+
this._loadTargetingIfNeeded();
|
|
24858
25749
|
}.bind(this)).catch(function(error) {
|
|
24859
25750
|
this.markFetchComplete();
|
|
24860
25751
|
logger.error(error);
|
|
@@ -24867,15 +25758,186 @@
|
|
|
24867
25758
|
return this.fetchPromise;
|
|
24868
25759
|
};
|
|
24869
25760
|
|
|
24870
|
-
FeatureFlagManager.prototype.markFetchComplete = function() {
|
|
24871
|
-
if (!this._fetchInProgressStartTime) {
|
|
24872
|
-
logger.error('Fetch in progress started time not set, cannot mark fetch complete');
|
|
24873
|
-
return;
|
|
24874
|
-
}
|
|
24875
|
-
this._fetchStartTime = this._fetchInProgressStartTime;
|
|
24876
|
-
this._fetchCompleteTime = Date.now();
|
|
24877
|
-
this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
|
|
24878
|
-
this._fetchInProgressStartTime = null;
|
|
25761
|
+
FeatureFlagManager.prototype.markFetchComplete = function() {
|
|
25762
|
+
if (!this._fetchInProgressStartTime) {
|
|
25763
|
+
logger.error('Fetch in progress started time not set, cannot mark fetch complete');
|
|
25764
|
+
return;
|
|
25765
|
+
}
|
|
25766
|
+
this._fetchStartTime = this._fetchInProgressStartTime;
|
|
25767
|
+
this._fetchCompleteTime = Date.now();
|
|
25768
|
+
this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
|
|
25769
|
+
this._fetchInProgressStartTime = null;
|
|
25770
|
+
};
|
|
25771
|
+
|
|
25772
|
+
/**
|
|
25773
|
+
* Proactively load targeting bundle if any pending events have property filters
|
|
25774
|
+
*/
|
|
25775
|
+
FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
|
|
25776
|
+
var hasPropertyFilters = false;
|
|
25777
|
+
_.each(this.pendingFirstTimeEvents, function(evt) {
|
|
25778
|
+
if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
|
|
25779
|
+
hasPropertyFilters = true;
|
|
25780
|
+
}
|
|
25781
|
+
});
|
|
25782
|
+
|
|
25783
|
+
if (hasPropertyFilters) {
|
|
25784
|
+
this.getTargeting().then(function() {
|
|
25785
|
+
logger.log('targeting loaded for property filter evaluation');
|
|
25786
|
+
});
|
|
25787
|
+
}
|
|
25788
|
+
};
|
|
25789
|
+
|
|
25790
|
+
/**
|
|
25791
|
+
* Get the targeting library (initializes if not already loaded)
|
|
25792
|
+
* This method is primarily for testing - production code should rely on automatic loading
|
|
25793
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
25794
|
+
*/
|
|
25795
|
+
FeatureFlagManager.prototype.getTargeting = function() {
|
|
25796
|
+
return getTargetingPromise(
|
|
25797
|
+
this.loadExtraBundle.bind(this),
|
|
25798
|
+
this.targetingSrc
|
|
25799
|
+
).catch(function(error) {
|
|
25800
|
+
logger.error('Failed to load targeting: ' + error);
|
|
25801
|
+
}.bind(this));
|
|
25802
|
+
};
|
|
25803
|
+
|
|
25804
|
+
/**
|
|
25805
|
+
* Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
|
|
25806
|
+
* @param {string} eventName - The name of the event being tracked
|
|
25807
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
25808
|
+
*
|
|
25809
|
+
* When a match is found (event name matches and property filters pass), this method:
|
|
25810
|
+
* - Switches the flag to the pending variant
|
|
25811
|
+
* - Marks the event as activated for this session
|
|
25812
|
+
* - Records the activation via the API (fire-and-forget)
|
|
25813
|
+
*/
|
|
25814
|
+
FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
|
|
25815
|
+
if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
|
|
25816
|
+
return;
|
|
25817
|
+
}
|
|
25818
|
+
|
|
25819
|
+
// Check if targeting promise exists (either bundled or async loaded)
|
|
25820
|
+
if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
|
|
25821
|
+
win[TARGETING_GLOBAL_NAME].then(function(library) {
|
|
25822
|
+
this._processFirstTimeEventCheck(eventName, properties, library);
|
|
25823
|
+
}.bind(this)).catch(function() {
|
|
25824
|
+
// If targeting failed to load, process with null
|
|
25825
|
+
// Events without property filters will still match
|
|
25826
|
+
this._processFirstTimeEventCheck(eventName, properties, null);
|
|
25827
|
+
}.bind(this));
|
|
25828
|
+
} else {
|
|
25829
|
+
// No targeting available, process with null
|
|
25830
|
+
// Events without property filters will still match
|
|
25831
|
+
this._processFirstTimeEventCheck(eventName, properties, null);
|
|
25832
|
+
}
|
|
25833
|
+
};
|
|
25834
|
+
|
|
25835
|
+
/**
|
|
25836
|
+
* Internal method to process first-time event checks with loaded targeting library
|
|
25837
|
+
* @param {string} eventName - The name of the event being tracked
|
|
25838
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
25839
|
+
* @param {Object} targeting - The loaded targeting library
|
|
25840
|
+
*/
|
|
25841
|
+
FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
|
|
25842
|
+
_.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
|
|
25843
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
25844
|
+
return;
|
|
25845
|
+
}
|
|
25846
|
+
|
|
25847
|
+
var flagKey = pendingEvent['flag_key'];
|
|
25848
|
+
|
|
25849
|
+
// Use targeting module to check if event matches
|
|
25850
|
+
var matchResult;
|
|
25851
|
+
|
|
25852
|
+
// If no targeting library and event has property filters, skip it
|
|
25853
|
+
if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
|
|
25854
|
+
logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
|
|
25855
|
+
return;
|
|
25856
|
+
}
|
|
25857
|
+
|
|
25858
|
+
// For simple events (no property filters), just check event name
|
|
25859
|
+
if (!targeting) {
|
|
25860
|
+
matchResult = {
|
|
25861
|
+
matches: eventName === pendingEvent['event_name'],
|
|
25862
|
+
error: null
|
|
25863
|
+
};
|
|
25864
|
+
} else {
|
|
25865
|
+
var criteria = {
|
|
25866
|
+
'event_name': pendingEvent['event_name'],
|
|
25867
|
+
'property_filters': pendingEvent['property_filters']
|
|
25868
|
+
};
|
|
25869
|
+
matchResult = targeting['eventMatchesCriteria'](
|
|
25870
|
+
eventName,
|
|
25871
|
+
properties,
|
|
25872
|
+
criteria
|
|
25873
|
+
);
|
|
25874
|
+
}
|
|
25875
|
+
|
|
25876
|
+
if (matchResult.error) {
|
|
25877
|
+
logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
|
|
25878
|
+
return;
|
|
25879
|
+
}
|
|
25880
|
+
|
|
25881
|
+
if (!matchResult.matches) {
|
|
25882
|
+
return;
|
|
25883
|
+
}
|
|
25884
|
+
|
|
25885
|
+
logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
|
|
25886
|
+
|
|
25887
|
+
var newVariant = {
|
|
25888
|
+
'key': pendingEvent['pending_variant']['variant_key'],
|
|
25889
|
+
'value': pendingEvent['pending_variant']['variant_value'],
|
|
25890
|
+
'experiment_id': pendingEvent['pending_variant']['experiment_id'],
|
|
25891
|
+
'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
|
|
25892
|
+
};
|
|
25893
|
+
|
|
25894
|
+
this.flags.set(flagKey, newVariant);
|
|
25895
|
+
this.activatedFirstTimeEvents[eventKey] = true;
|
|
25896
|
+
|
|
25897
|
+
this.recordFirstTimeEvent(
|
|
25898
|
+
pendingEvent['flag_id'],
|
|
25899
|
+
pendingEvent['project_id'],
|
|
25900
|
+
pendingEvent['first_time_event_hash']
|
|
25901
|
+
);
|
|
25902
|
+
}, this);
|
|
25903
|
+
};
|
|
25904
|
+
|
|
25905
|
+
FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
|
|
25906
|
+
// Construct URL: {api_host}/flags/{flagId}/first-time-events
|
|
25907
|
+
return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
|
|
25908
|
+
};
|
|
25909
|
+
|
|
25910
|
+
FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
|
|
25911
|
+
var distinctId = this.getMpProperty('distinct_id');
|
|
25912
|
+
var traceparent = generateTraceparent();
|
|
25913
|
+
|
|
25914
|
+
// Build URL with query string parameters
|
|
25915
|
+
var searchParams = new URLSearchParams();
|
|
25916
|
+
searchParams.set('mp_lib', 'web');
|
|
25917
|
+
searchParams.set('$lib_version', Config.LIB_VERSION);
|
|
25918
|
+
var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
|
|
25919
|
+
|
|
25920
|
+
var payload = {
|
|
25921
|
+
'distinct_id': distinctId,
|
|
25922
|
+
'project_id': projectId,
|
|
25923
|
+
'first_time_event_hash': firstTimeEventHash
|
|
25924
|
+
};
|
|
25925
|
+
|
|
25926
|
+
logger.log('Recording first-time event for flag: ' + flagId);
|
|
25927
|
+
|
|
25928
|
+
// Fire-and-forget POST request
|
|
25929
|
+
this.fetch.call(win, url, {
|
|
25930
|
+
'method': 'POST',
|
|
25931
|
+
'headers': {
|
|
25932
|
+
'Content-Type': 'application/json',
|
|
25933
|
+
'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
|
|
25934
|
+
'traceparent': traceparent
|
|
25935
|
+
},
|
|
25936
|
+
'body': JSON.stringify(payload)
|
|
25937
|
+
}).catch(function(error) {
|
|
25938
|
+
// Silent failure - cohort sync will catch up
|
|
25939
|
+
logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
|
|
25940
|
+
});
|
|
24879
25941
|
};
|
|
24880
25942
|
|
|
24881
25943
|
FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
|
|
@@ -24996,6 +26058,217 @@
|
|
|
24996
26058
|
// Deprecated method
|
|
24997
26059
|
FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
|
|
24998
26060
|
|
|
26061
|
+
// Exports intended only for testing
|
|
26062
|
+
FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
|
|
26063
|
+
|
|
26064
|
+
/* eslint camelcase: "off" */
|
|
26065
|
+
|
|
26066
|
+
|
|
26067
|
+
/**
|
|
26068
|
+
* RecorderManager: manages session recording initialization, lifecycle and state
|
|
26069
|
+
* @constructor
|
|
26070
|
+
*/
|
|
26071
|
+
var RecorderManager = function(initOptions) {
|
|
26072
|
+
// TODO - Passing in mixpanel instance as it is still needed for recorder creation
|
|
26073
|
+
// but ideally we should be able to remove this dependency.
|
|
26074
|
+
this.mixpanelInstance = initOptions.mixpanelInstance;
|
|
26075
|
+
|
|
26076
|
+
this.getMpConfig = initOptions.getConfigFunc;
|
|
26077
|
+
this.getTabId = initOptions.getTabIdFunc;
|
|
26078
|
+
this.reportError = initOptions.reportErrorFunc;
|
|
26079
|
+
this.getDistinctId = initOptions.getDistinctIdFunc;
|
|
26080
|
+
this.loadExtraBundle = initOptions.loadExtraBundle;
|
|
26081
|
+
this.recorderSrc = initOptions.recorderSrc;
|
|
26082
|
+
this.targetingSrc = initOptions.targetingSrc;
|
|
26083
|
+
this.libBasePath = initOptions.libBasePath;
|
|
26084
|
+
|
|
26085
|
+
this._recorder = null;
|
|
26086
|
+
};
|
|
26087
|
+
|
|
26088
|
+
RecorderManager.prototype.shouldLoadRecorder = function() {
|
|
26089
|
+
if (this.getMpConfig('disable_persistence')) {
|
|
26090
|
+
console$1.log('Load recorder check skipped due to disable_persistence config');
|
|
26091
|
+
return PromisePolyfill.resolve(false);
|
|
26092
|
+
}
|
|
26093
|
+
|
|
26094
|
+
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
26095
|
+
var tab_id = this.getTabId();
|
|
26096
|
+
return recording_registry_idb.init()
|
|
26097
|
+
.then(function () {
|
|
26098
|
+
return recording_registry_idb.getAll();
|
|
26099
|
+
})
|
|
26100
|
+
.then(function (recordings) {
|
|
26101
|
+
for (var i = 0; i < recordings.length; i++) {
|
|
26102
|
+
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
26103
|
+
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
26104
|
+
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
26105
|
+
return true;
|
|
26106
|
+
}
|
|
26107
|
+
}
|
|
26108
|
+
return false;
|
|
26109
|
+
})
|
|
26110
|
+
.catch(_.bind(function (err) {
|
|
26111
|
+
this.reportError('Error checking recording registry', err);
|
|
26112
|
+
return false;
|
|
26113
|
+
}, this));
|
|
26114
|
+
};
|
|
26115
|
+
|
|
26116
|
+
RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
|
|
26117
|
+
if (!win['MutationObserver']) {
|
|
26118
|
+
console$1.critical('Browser does not support MutationObserver; skipping session recording');
|
|
26119
|
+
return PromisePolyfill.resolve();
|
|
26120
|
+
}
|
|
26121
|
+
|
|
26122
|
+
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
26123
|
+
return new PromisePolyfill(_.bind(function(resolve) {
|
|
26124
|
+
var handleLoadedRecorder = safewrap(_.bind(function() {
|
|
26125
|
+
this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
|
|
26126
|
+
this._recorder['resumeRecording'](startNewIfInactive);
|
|
26127
|
+
resolve();
|
|
26128
|
+
}, this));
|
|
26129
|
+
|
|
26130
|
+
if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
|
|
26131
|
+
var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
|
|
26132
|
+
this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
|
|
26133
|
+
} else {
|
|
26134
|
+
handleLoadedRecorder();
|
|
26135
|
+
}
|
|
26136
|
+
}, this));
|
|
26137
|
+
}, this);
|
|
26138
|
+
|
|
26139
|
+
/**
|
|
26140
|
+
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
26141
|
+
* 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.
|
|
26142
|
+
*/
|
|
26143
|
+
var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
|
|
26144
|
+
var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
|
|
26145
|
+
if (force_start || is_sampled) {
|
|
26146
|
+
return loadRecorder(true);
|
|
26147
|
+
} else {
|
|
26148
|
+
return this.shouldLoadRecorder()
|
|
26149
|
+
.then(_.bind(function (shouldLoad) {
|
|
26150
|
+
if (shouldLoad) {
|
|
26151
|
+
return loadRecorder(false);
|
|
26152
|
+
}
|
|
26153
|
+
return PromisePolyfill.resolve();
|
|
26154
|
+
}, this));
|
|
26155
|
+
}
|
|
26156
|
+
};
|
|
26157
|
+
|
|
26158
|
+
RecorderManager.prototype.isRecording = function() {
|
|
26159
|
+
// Safety check: ensure isRecording method exists (older CDN builds may not have it)
|
|
26160
|
+
if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
|
|
26161
|
+
return false;
|
|
26162
|
+
}
|
|
26163
|
+
try {
|
|
26164
|
+
return this._recorder['isRecording']();
|
|
26165
|
+
} catch (e) {
|
|
26166
|
+
this.reportError('Error checking if recording is active', e);
|
|
26167
|
+
return false;
|
|
26168
|
+
}
|
|
26169
|
+
};
|
|
26170
|
+
|
|
26171
|
+
RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
|
|
26172
|
+
var isRecording = this.isRecording();
|
|
26173
|
+
var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
|
|
26174
|
+
|
|
26175
|
+
if (!isRecording && recordingTriggerEvents) {
|
|
26176
|
+
var trigger = recordingTriggerEvents[event_name];
|
|
26177
|
+
if (trigger && typeof trigger['percentage'] === 'number') {
|
|
26178
|
+
var newRate = trigger['percentage'];
|
|
26179
|
+
var propertyFilters = trigger['property_filters'];
|
|
26180
|
+
if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
|
|
26181
|
+
var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
|
|
26182
|
+
getTargetingPromise(this.loadExtraBundle, targetingSrc)
|
|
26183
|
+
.then(function(targeting) {
|
|
26184
|
+
try {
|
|
26185
|
+
var result = targeting['eventMatchesCriteria'](
|
|
26186
|
+
event_name,
|
|
26187
|
+
properties,
|
|
26188
|
+
{
|
|
26189
|
+
'event_name': event_name,
|
|
26190
|
+
'property_filters': propertyFilters
|
|
26191
|
+
}
|
|
26192
|
+
);
|
|
26193
|
+
if (result['matches']) {
|
|
26194
|
+
this.checkAndStartSessionRecording(false, newRate);
|
|
26195
|
+
}
|
|
26196
|
+
} catch (err) {
|
|
26197
|
+
console$1.critical('Could not parse recording event trigger properties logic:', err);
|
|
26198
|
+
}
|
|
26199
|
+
}.bind(this)).catch(function(err) {
|
|
26200
|
+
console$1.critical('Failed to load targeting library:', err);
|
|
26201
|
+
});
|
|
26202
|
+
} else {
|
|
26203
|
+
this.checkAndStartSessionRecording(false, newRate);
|
|
26204
|
+
}
|
|
26205
|
+
}
|
|
26206
|
+
}
|
|
26207
|
+
};
|
|
26208
|
+
|
|
26209
|
+
RecorderManager.prototype.stopSessionRecording = function() {
|
|
26210
|
+
if (this._recorder) {
|
|
26211
|
+
return this._recorder['stopRecording']();
|
|
26212
|
+
}
|
|
26213
|
+
return PromisePolyfill.resolve();
|
|
26214
|
+
};
|
|
26215
|
+
|
|
26216
|
+
RecorderManager.prototype.pauseSessionRecording = function() {
|
|
26217
|
+
if (this._recorder) {
|
|
26218
|
+
return this._recorder['pauseRecording']();
|
|
26219
|
+
}
|
|
26220
|
+
return PromisePolyfill.resolve();
|
|
26221
|
+
};
|
|
26222
|
+
|
|
26223
|
+
RecorderManager.prototype.resumeSessionRecording = function() {
|
|
26224
|
+
if (this._recorder) {
|
|
26225
|
+
return this._recorder['resumeRecording']();
|
|
26226
|
+
}
|
|
26227
|
+
return PromisePolyfill.resolve();
|
|
26228
|
+
};
|
|
26229
|
+
|
|
26230
|
+
RecorderManager.prototype.isRecordingHeatmapData = function() {
|
|
26231
|
+
return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
|
|
26232
|
+
};
|
|
26233
|
+
|
|
26234
|
+
RecorderManager.prototype.getSessionRecordingProperties = function() {
|
|
26235
|
+
var props = {};
|
|
26236
|
+
var replay_id = this.getSessionReplayId();
|
|
26237
|
+
if (replay_id) {
|
|
26238
|
+
props['$mp_replay_id'] = replay_id;
|
|
26239
|
+
}
|
|
26240
|
+
return props;
|
|
26241
|
+
};
|
|
26242
|
+
|
|
26243
|
+
RecorderManager.prototype.getSessionReplayUrl = function() {
|
|
26244
|
+
var replay_url = null;
|
|
26245
|
+
var replay_id = this.getSessionReplayId();
|
|
26246
|
+
if (replay_id) {
|
|
26247
|
+
var query_params = _.HTTPBuildQuery({
|
|
26248
|
+
'replay_id': replay_id,
|
|
26249
|
+
'distinct_id': this.getDistinctId(),
|
|
26250
|
+
'token': this.getMpConfig('token')
|
|
26251
|
+
});
|
|
26252
|
+
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
26253
|
+
}
|
|
26254
|
+
return replay_url;
|
|
26255
|
+
};
|
|
26256
|
+
|
|
26257
|
+
RecorderManager.prototype.getSessionReplayId = function() {
|
|
26258
|
+
var replay_id = null;
|
|
26259
|
+
if (this._recorder) {
|
|
26260
|
+
replay_id = this._recorder['replayId'];
|
|
26261
|
+
}
|
|
26262
|
+
return replay_id || null;
|
|
26263
|
+
};
|
|
26264
|
+
|
|
26265
|
+
// "private" public method to reach into the recorder in test cases
|
|
26266
|
+
RecorderManager.prototype.getRecorder = function() {
|
|
26267
|
+
return this._recorder;
|
|
26268
|
+
};
|
|
26269
|
+
|
|
26270
|
+
safewrapClass(RecorderManager);
|
|
26271
|
+
|
|
24999
26272
|
/* eslint camelcase: "off" */
|
|
25000
26273
|
|
|
25001
26274
|
|
|
@@ -26459,12 +27732,17 @@
|
|
|
26459
27732
|
'record_collect_fonts': false,
|
|
26460
27733
|
'record_console': true,
|
|
26461
27734
|
'record_heatmap_data': false,
|
|
27735
|
+
'recording_event_triggers': {},
|
|
26462
27736
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
26463
27737
|
'record_mask_inputs': true,
|
|
26464
27738
|
'record_max_ms': MAX_RECORDING_MS,
|
|
26465
27739
|
'record_min_ms': 0,
|
|
27740
|
+
'record_network': false,
|
|
27741
|
+
'record_network_options': {},
|
|
26466
27742
|
'record_sessions_percent': 0,
|
|
26467
|
-
'recorder_src':
|
|
27743
|
+
'recorder_src': null,
|
|
27744
|
+
'targeting_src': null,
|
|
27745
|
+
'lib_base_path': 'https://cdn.mxpnl.com/libs/',
|
|
26468
27746
|
'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
|
|
26469
27747
|
};
|
|
26470
27748
|
|
|
@@ -26618,6 +27896,19 @@
|
|
|
26618
27896
|
'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
|
|
26619
27897
|
}));
|
|
26620
27898
|
|
|
27899
|
+
this.recorderManager = new RecorderManager({
|
|
27900
|
+
mixpanelInstance: this,
|
|
27901
|
+
getConfigFunc: _.bind(this.get_config, this),
|
|
27902
|
+
setConfigFunc: _.bind(this.set_config, this),
|
|
27903
|
+
getTabIdFunc: _.bind(this.get_tab_id, this),
|
|
27904
|
+
reportErrorFunc: _.bind(this.report_error, this),
|
|
27905
|
+
getDistinctIdFunc: _.bind(this.get_distinct_id, this),
|
|
27906
|
+
recorderSrc: this.get_config('recorder_src'),
|
|
27907
|
+
targetingSrc: this.get_config('targeting_src'),
|
|
27908
|
+
libBasePath: this.get_config('lib_base_path'),
|
|
27909
|
+
loadExtraBundle: load_extra_bundle
|
|
27910
|
+
});
|
|
27911
|
+
|
|
26621
27912
|
this['_jsc'] = NOOP_FUNC;
|
|
26622
27913
|
|
|
26623
27914
|
this.__dom_loaded_queue = [];
|
|
@@ -26694,7 +27985,9 @@
|
|
|
26694
27985
|
getConfigFunc: _.bind(this.get_config, this),
|
|
26695
27986
|
setConfigFunc: _.bind(this.set_config, this),
|
|
26696
27987
|
getPropertyFunc: _.bind(this.get_property, this),
|
|
26697
|
-
trackingFunc: _.bind(this.track, this)
|
|
27988
|
+
trackingFunc: _.bind(this.track, this),
|
|
27989
|
+
loadExtraBundle: load_extra_bundle,
|
|
27990
|
+
targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
|
|
26698
27991
|
});
|
|
26699
27992
|
this.flags.init();
|
|
26700
27993
|
this['flags'] = this.flags;
|
|
@@ -26707,11 +28000,11 @@
|
|
|
26707
28000
|
// Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
|
|
26708
28001
|
var mode = this.get_config('remote_settings_mode');
|
|
26709
28002
|
if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
|
|
26710
|
-
this._fetch_remote_settings(mode).then(_.bind(function() {
|
|
26711
|
-
this._check_and_start_session_recording();
|
|
28003
|
+
this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
|
|
28004
|
+
return this._check_and_start_session_recording();
|
|
26712
28005
|
}, this));
|
|
26713
28006
|
} else {
|
|
26714
|
-
this._check_and_start_session_recording();
|
|
28007
|
+
this.__session_recording_init_promise = this._check_and_start_session_recording();
|
|
26715
28008
|
}
|
|
26716
28009
|
};
|
|
26717
28010
|
|
|
@@ -26755,132 +28048,50 @@
|
|
|
26755
28048
|
return this.tab_id || null;
|
|
26756
28049
|
};
|
|
26757
28050
|
|
|
26758
|
-
MixpanelLib.prototype._should_load_recorder = function () {
|
|
26759
|
-
if (this.get_config('disable_persistence')) {
|
|
26760
|
-
console$1.log('Load recorder check skipped due to disable_persistence config');
|
|
26761
|
-
return Promise.resolve(false);
|
|
26762
|
-
}
|
|
26763
|
-
|
|
26764
|
-
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
26765
|
-
var tab_id = this.get_tab_id();
|
|
26766
|
-
return recording_registry_idb.init()
|
|
26767
|
-
.then(function () {
|
|
26768
|
-
return recording_registry_idb.getAll();
|
|
26769
|
-
})
|
|
26770
|
-
.then(function (recordings) {
|
|
26771
|
-
for (var i = 0; i < recordings.length; i++) {
|
|
26772
|
-
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
26773
|
-
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
26774
|
-
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
26775
|
-
return true;
|
|
26776
|
-
}
|
|
26777
|
-
}
|
|
26778
|
-
return false;
|
|
26779
|
-
})
|
|
26780
|
-
.catch(_.bind(function (err) {
|
|
26781
|
-
this.report_error('Error checking recording registry', err);
|
|
26782
|
-
}, this));
|
|
26783
|
-
};
|
|
26784
|
-
|
|
26785
28051
|
MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
|
|
26786
|
-
|
|
26787
|
-
console$1.critical('Browser does not support MutationObserver; skipping session recording');
|
|
26788
|
-
return;
|
|
26789
|
-
}
|
|
26790
|
-
|
|
26791
|
-
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
26792
|
-
var handleLoadedRecorder = _.bind(function() {
|
|
26793
|
-
this._recorder = this._recorder || new win['__mp_recorder'](this);
|
|
26794
|
-
this._recorder['resumeRecording'](startNewIfInactive);
|
|
26795
|
-
}, this);
|
|
26796
|
-
|
|
26797
|
-
if (_.isUndefined(win['__mp_recorder'])) {
|
|
26798
|
-
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
26799
|
-
} else {
|
|
26800
|
-
handleLoadedRecorder();
|
|
26801
|
-
}
|
|
26802
|
-
}, this);
|
|
26803
|
-
|
|
26804
|
-
/**
|
|
26805
|
-
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
26806
|
-
* 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.
|
|
26807
|
-
*/
|
|
26808
|
-
var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
|
|
26809
|
-
if (force_start || is_sampled) {
|
|
26810
|
-
loadRecorder(true);
|
|
26811
|
-
} else {
|
|
26812
|
-
this._should_load_recorder()
|
|
26813
|
-
.then(function (shouldLoad) {
|
|
26814
|
-
if (shouldLoad) {
|
|
26815
|
-
loadRecorder(false);
|
|
26816
|
-
}
|
|
26817
|
-
});
|
|
26818
|
-
}
|
|
28052
|
+
return this.recorderManager.checkAndStartSessionRecording(force_start);
|
|
26819
28053
|
});
|
|
26820
28054
|
|
|
28055
|
+
MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
|
|
28056
|
+
return this.recorderManager.startRecordingOnEvent(event_name, properties);
|
|
28057
|
+
};
|
|
28058
|
+
|
|
26821
28059
|
MixpanelLib.prototype.start_session_recording = function () {
|
|
26822
|
-
this._check_and_start_session_recording(true);
|
|
28060
|
+
return this._check_and_start_session_recording(true);
|
|
26823
28061
|
};
|
|
26824
28062
|
|
|
26825
28063
|
MixpanelLib.prototype.stop_session_recording = function () {
|
|
26826
|
-
|
|
26827
|
-
return this._recorder['stopRecording']();
|
|
26828
|
-
}
|
|
26829
|
-
return Promise.resolve();
|
|
28064
|
+
return this.recorderManager.stopSessionRecording();
|
|
26830
28065
|
};
|
|
26831
28066
|
|
|
26832
28067
|
MixpanelLib.prototype.pause_session_recording = function () {
|
|
26833
|
-
|
|
26834
|
-
return this._recorder['pauseRecording']();
|
|
26835
|
-
}
|
|
26836
|
-
return Promise.resolve();
|
|
28068
|
+
return this.recorderManager.pauseSessionRecording();
|
|
26837
28069
|
};
|
|
26838
28070
|
|
|
26839
28071
|
MixpanelLib.prototype.resume_session_recording = function () {
|
|
26840
|
-
|
|
26841
|
-
return this._recorder['resumeRecording']();
|
|
26842
|
-
}
|
|
26843
|
-
return Promise.resolve();
|
|
28072
|
+
return this.recorderManager.resumeSessionRecording();
|
|
26844
28073
|
};
|
|
26845
28074
|
|
|
26846
28075
|
MixpanelLib.prototype.is_recording_heatmap_data = function () {
|
|
26847
|
-
return this.
|
|
28076
|
+
return this.recorderManager.isRecordingHeatmapData();
|
|
26848
28077
|
};
|
|
26849
28078
|
|
|
26850
28079
|
MixpanelLib.prototype.get_session_recording_properties = function () {
|
|
26851
|
-
|
|
26852
|
-
var replay_id = this._get_session_replay_id();
|
|
26853
|
-
if (replay_id) {
|
|
26854
|
-
props['$mp_replay_id'] = replay_id;
|
|
26855
|
-
}
|
|
26856
|
-
return props;
|
|
28080
|
+
return this.recorderManager.getSessionRecordingProperties();
|
|
26857
28081
|
};
|
|
26858
28082
|
|
|
26859
28083
|
MixpanelLib.prototype.get_session_replay_url = function () {
|
|
26860
|
-
|
|
26861
|
-
var replay_id = this._get_session_replay_id();
|
|
26862
|
-
if (replay_id) {
|
|
26863
|
-
var query_params = _.HTTPBuildQuery({
|
|
26864
|
-
'replay_id': replay_id,
|
|
26865
|
-
'distinct_id': this.get_distinct_id(),
|
|
26866
|
-
'token': this.get_config('token')
|
|
26867
|
-
});
|
|
26868
|
-
replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
|
|
26869
|
-
}
|
|
26870
|
-
return replay_url;
|
|
26871
|
-
};
|
|
26872
|
-
|
|
26873
|
-
MixpanelLib.prototype._get_session_replay_id = function () {
|
|
26874
|
-
var replay_id = null;
|
|
26875
|
-
if (this._recorder) {
|
|
26876
|
-
replay_id = this._recorder['replayId'];
|
|
26877
|
-
}
|
|
26878
|
-
return replay_id || null;
|
|
28084
|
+
return this.recorderManager.getSessionReplayUrl();
|
|
26879
28085
|
};
|
|
26880
28086
|
|
|
26881
28087
|
// "private" public method to reach into the recorder in test cases
|
|
26882
28088
|
MixpanelLib.prototype.__get_recorder = function () {
|
|
26883
|
-
return this.
|
|
28089
|
+
return this.recorderManager.getRecorder();
|
|
28090
|
+
};
|
|
28091
|
+
|
|
28092
|
+
// "private" public method to get session recording init promise in test cases
|
|
28093
|
+
MixpanelLib.prototype.__get_recording_init_promise = function () {
|
|
28094
|
+
return this.__session_recording_init_promise;
|
|
26884
28095
|
};
|
|
26885
28096
|
|
|
26886
28097
|
// Private methods
|
|
@@ -27138,6 +28349,7 @@
|
|
|
27138
28349
|
};
|
|
27139
28350
|
|
|
27140
28351
|
MixpanelLib.prototype._fetch_remote_settings = function(mode) {
|
|
28352
|
+
var self = this;
|
|
27141
28353
|
var disableRecordingIfStrict = function() {
|
|
27142
28354
|
if (mode === 'strict') {
|
|
27143
28355
|
self.set_config({'record_sessions_percent': 0});
|
|
@@ -27158,7 +28370,6 @@
|
|
|
27158
28370
|
};
|
|
27159
28371
|
var query_string = _.HTTPBuildQuery(request_params);
|
|
27160
28372
|
var full_url = settings_endpoint + '?' + query_string;
|
|
27161
|
-
var self = this;
|
|
27162
28373
|
|
|
27163
28374
|
var abortController = new AbortController();
|
|
27164
28375
|
var timeout_id = setTimeout(function() {
|
|
@@ -27350,6 +28561,34 @@
|
|
|
27350
28561
|
this._execute_array([item]);
|
|
27351
28562
|
};
|
|
27352
28563
|
|
|
28564
|
+
/**
|
|
28565
|
+
* Enables events on the Mixpanel object. If passed no arguments,
|
|
28566
|
+
* this function enable tracking of all events. If passed an
|
|
28567
|
+
* array of event names, those events will be enabled, but other
|
|
28568
|
+
* existing disabled events will continue to be not tracked.
|
|
28569
|
+
*
|
|
28570
|
+
* @param {Array} [events] An array of event names to enable
|
|
28571
|
+
*/
|
|
28572
|
+
MixpanelLib.prototype.enable = function(events) {
|
|
28573
|
+
var keys, new_disabled_events, i, j;
|
|
28574
|
+
|
|
28575
|
+
if (typeof(events) === 'undefined') {
|
|
28576
|
+
this._flags.disable_all_events = false;
|
|
28577
|
+
} else {
|
|
28578
|
+
keys = {};
|
|
28579
|
+
new_disabled_events = [];
|
|
28580
|
+
for (i = 0; i < events.length; i++) {
|
|
28581
|
+
keys[events[i]] = true;
|
|
28582
|
+
}
|
|
28583
|
+
for (j = 0; j < this.__disabled_events.length; j++) {
|
|
28584
|
+
if (!keys[this.__disabled_events[j]]) {
|
|
28585
|
+
new_disabled_events.push(this.__disabled_events[j]);
|
|
28586
|
+
}
|
|
28587
|
+
}
|
|
28588
|
+
this.__disabled_events = new_disabled_events;
|
|
28589
|
+
}
|
|
28590
|
+
};
|
|
28591
|
+
|
|
27353
28592
|
/**
|
|
27354
28593
|
* Disable events on the Mixpanel object. If passed no arguments,
|
|
27355
28594
|
* this function disables tracking of any event. If passed an
|
|
@@ -27523,6 +28762,8 @@
|
|
|
27523
28762
|
this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
|
|
27524
28763
|
}
|
|
27525
28764
|
|
|
28765
|
+
this._start_recording_on_event(event_name, properties);
|
|
28766
|
+
|
|
27526
28767
|
var data = {
|
|
27527
28768
|
'event': event_name,
|
|
27528
28769
|
'properties': properties
|
|
@@ -27536,6 +28777,11 @@
|
|
|
27536
28777
|
send_request_options: options
|
|
27537
28778
|
}, callback);
|
|
27538
28779
|
|
|
28780
|
+
// Check for first-time event matches
|
|
28781
|
+
if (this.flags && this.flags.checkFirstTimeEvents) {
|
|
28782
|
+
this.flags.checkFirstTimeEvents(event_name, properties);
|
|
28783
|
+
}
|
|
28784
|
+
|
|
27539
28785
|
return ret;
|
|
27540
28786
|
});
|
|
27541
28787
|
|
|
@@ -28726,6 +29972,7 @@
|
|
|
28726
29972
|
// MixpanelLib Exports
|
|
28727
29973
|
MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
|
|
28728
29974
|
MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
|
|
29975
|
+
MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
|
|
28729
29976
|
MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
|
|
28730
29977
|
MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
|
|
28731
29978
|
MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
|
|
@@ -28769,6 +30016,7 @@
|
|
|
28769
30016
|
|
|
28770
30017
|
// Exports intended only for testing
|
|
28771
30018
|
MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
|
|
30019
|
+
MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
|
|
28772
30020
|
|
|
28773
30021
|
// MixpanelPersistence Exports
|
|
28774
30022
|
MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
|