mixpanel-browser 2.76.0 → 2.78.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/dependabot.yml +8 -0
- package/.github/workflows/integration-tests.yml +2 -2
- package/.github/workflows/unit-tests.yml +2 -2
- package/CHANGELOG.md +8 -0
- package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js +2 -0
- package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js.map +1 -0
- package/dist/async-modules/{mixpanel-recorder-bIS4LMGd.js → mixpanel-recorder-zMBXIyeG.js} +84 -10
- package/dist/async-modules/{mixpanel-targeting-VOeN7RWY.min.js → mixpanel-targeting-BSHal4N9.min.js} +2 -2
- package/dist/async-modules/{mixpanel-targeting-VOeN7RWY.min.js.map → mixpanel-targeting-BSHal4N9.min.js.map} +1 -1
- package/dist/async-modules/{mixpanel-targeting-BcAPS-Mz.js → mixpanel-targeting-UHf4eBfC.js} +1 -1
- package/dist/mixpanel-core.cjs.d.ts +3 -1
- package/dist/mixpanel-core.cjs.js +292 -130
- package/dist/mixpanel-recorder.js +84 -10
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +1 -1
- package/dist/mixpanel-targeting.min.js +1 -1
- package/dist/mixpanel-targeting.min.js.map +1 -1
- package/dist/mixpanel-with-async-modules.cjs.d.ts +3 -1
- package/dist/mixpanel-with-async-modules.cjs.js +294 -132
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +3 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +294 -132
- package/dist/mixpanel-with-recorder.d.ts +3 -1
- package/dist/mixpanel-with-recorder.js +381 -168
- package/dist/mixpanel-with-recorder.min.d.ts +3 -1
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +3 -1
- package/dist/mixpanel.amd.js +381 -168
- package/dist/mixpanel.cjs.d.ts +3 -1
- package/dist/mixpanel.cjs.js +381 -168
- package/dist/mixpanel.globals.js +294 -132
- package/dist/mixpanel.min.js +191 -186
- package/dist/mixpanel.module.d.ts +3 -1
- package/dist/mixpanel.module.js +381 -168
- package/dist/mixpanel.umd.d.ts +3 -1
- package/dist/mixpanel.umd.js +381 -168
- package/dist/rrweb-bundled.js +61 -9
- package/dist/rrweb-compiled.js +56 -9
- package/package.json +6 -5
- package/src/config.js +1 -1
- package/src/flags/CLAUDE.md +24 -0
- package/src/flags/index.js +109 -80
- package/src/index.d.ts +3 -1
- package/src/mixpanel-core.js +4 -2
- package/src/recorder/session-recording.js +5 -1
- package/src/recorder/utils.js +27 -1
- package/src/recorder-manager.js +110 -2
- package/testServer.js +16 -1
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +0 -2
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +0 -1
- /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
package/dist/rrweb-bundled.js
CHANGED
|
@@ -9304,14 +9304,7 @@ class MutationBuffer {
|
|
|
9304
9304
|
};
|
|
9305
9305
|
while (this.mapRemoves.length) {
|
|
9306
9306
|
const removedNode = this.mapRemoves.shift();
|
|
9307
|
-
|
|
9308
|
-
try {
|
|
9309
|
-
this.iframeManager.removeIframe(removedNode);
|
|
9310
|
-
} catch (e2) {
|
|
9311
|
-
}
|
|
9312
|
-
} else {
|
|
9313
|
-
this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
|
|
9314
|
-
}
|
|
9307
|
+
this.cleanupRemovedNode(removedNode);
|
|
9315
9308
|
this.mirror.removeNodeFromMap(removedNode);
|
|
9316
9309
|
}
|
|
9317
9310
|
for (const n2 of this.movedSet) {
|
|
@@ -9620,6 +9613,22 @@ class MutationBuffer {
|
|
|
9620
9613
|
}
|
|
9621
9614
|
}
|
|
9622
9615
|
});
|
|
9616
|
+
__publicField$1(this, "cleanupRemovedNode", (node2) => {
|
|
9617
|
+
if (node2.nodeName === "IFRAME") {
|
|
9618
|
+
try {
|
|
9619
|
+
this.iframeManager.removeIframe(node2);
|
|
9620
|
+
} catch (e2) {
|
|
9621
|
+
}
|
|
9622
|
+
} else {
|
|
9623
|
+
try {
|
|
9624
|
+
this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
|
|
9625
|
+
} catch (e2) {
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
node2.childNodes.forEach((child) => {
|
|
9629
|
+
this.cleanupRemovedNode(child);
|
|
9630
|
+
});
|
|
9631
|
+
});
|
|
9623
9632
|
}
|
|
9624
9633
|
init(options) {
|
|
9625
9634
|
[
|
|
@@ -11844,6 +11853,35 @@ class ProcessedNodeManager {
|
|
|
11844
11853
|
destroy() {
|
|
11845
11854
|
}
|
|
11846
11855
|
}
|
|
11856
|
+
function toOrigin(url) {
|
|
11857
|
+
try {
|
|
11858
|
+
const origin = new URL(url).origin;
|
|
11859
|
+
return origin !== "null" ? origin : null;
|
|
11860
|
+
} catch {
|
|
11861
|
+
return null;
|
|
11862
|
+
}
|
|
11863
|
+
}
|
|
11864
|
+
function buildAllowedOriginSet(origins) {
|
|
11865
|
+
if (!Array.isArray(origins) || origins.length === 0) {
|
|
11866
|
+
throw new Error(
|
|
11867
|
+
"[rrweb] allowedIframeOrigins must be a non-empty array of origin strings."
|
|
11868
|
+
);
|
|
11869
|
+
}
|
|
11870
|
+
const set = /* @__PURE__ */ new Set();
|
|
11871
|
+
for (let i2 = 0; i2 < origins.length; i2++) {
|
|
11872
|
+
const entry = origins[i2];
|
|
11873
|
+
if (typeof entry !== "string") {
|
|
11874
|
+
throw new Error(
|
|
11875
|
+
`[rrweb] allowedIframeOrigins[${i2}] must be a string, got ${typeof entry}.`
|
|
11876
|
+
);
|
|
11877
|
+
}
|
|
11878
|
+
const origin = toOrigin(entry);
|
|
11879
|
+
if (origin) {
|
|
11880
|
+
set.add(origin);
|
|
11881
|
+
}
|
|
11882
|
+
}
|
|
11883
|
+
return Object.freeze(set);
|
|
11884
|
+
}
|
|
11847
11885
|
let wrappedEmit;
|
|
11848
11886
|
let takeFullSnapshot$1;
|
|
11849
11887
|
let canvasManager;
|
|
@@ -11884,6 +11922,7 @@ function record(options = {}) {
|
|
|
11884
11922
|
recordDOM = true,
|
|
11885
11923
|
recordCanvas = false,
|
|
11886
11924
|
recordCrossOriginIframes = false,
|
|
11925
|
+
allowedIframeOrigins,
|
|
11887
11926
|
recordAfter = options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load",
|
|
11888
11927
|
userTriggeredOnInput = false,
|
|
11889
11928
|
collectFonts = false,
|
|
@@ -11894,6 +11933,13 @@ function record(options = {}) {
|
|
|
11894
11933
|
errorHandler: errorHandler2
|
|
11895
11934
|
} = options;
|
|
11896
11935
|
registerErrorHandler(errorHandler2);
|
|
11936
|
+
let validatedOrigins;
|
|
11937
|
+
if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
|
|
11938
|
+
validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
|
|
11939
|
+
if (validatedOrigins.size === 0) {
|
|
11940
|
+
validatedOrigins = void 0;
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11897
11943
|
const inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
|
|
11898
11944
|
let passEmitsToParent = false;
|
|
11899
11945
|
if (!inEmittingFrame) {
|
|
@@ -11981,7 +12027,13 @@ function record(options = {}) {
|
|
|
11981
12027
|
origin: window.location.origin,
|
|
11982
12028
|
isCheckout
|
|
11983
12029
|
};
|
|
11984
|
-
|
|
12030
|
+
if (validatedOrigins) {
|
|
12031
|
+
for (const targetOrigin of validatedOrigins) {
|
|
12032
|
+
window.parent.postMessage(message, targetOrigin);
|
|
12033
|
+
}
|
|
12034
|
+
} else {
|
|
12035
|
+
window.parent.postMessage(message, "*");
|
|
12036
|
+
}
|
|
11985
12037
|
}
|
|
11986
12038
|
if (e2.type === EventType.FullSnapshot) {
|
|
11987
12039
|
lastFullSnapshotEvent = e2;
|
package/dist/rrweb-compiled.js
CHANGED
|
@@ -10695,13 +10695,7 @@ var MutationBuffer = /*#__PURE__*/ function() {
|
|
|
10695
10695
|
};
|
|
10696
10696
|
while(_this.mapRemoves.length){
|
|
10697
10697
|
var removedNode = _this.mapRemoves.shift();
|
|
10698
|
-
|
|
10699
|
-
try {
|
|
10700
|
-
_this.iframeManager.removeIframe(removedNode);
|
|
10701
|
-
} catch (e2) {}
|
|
10702
|
-
} else {
|
|
10703
|
-
_this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
|
|
10704
|
-
}
|
|
10698
|
+
_this.cleanupRemovedNode(removedNode);
|
|
10705
10699
|
_this.mirror.removeNodeFromMap(removedNode);
|
|
10706
10700
|
}
|
|
10707
10701
|
for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
|
|
@@ -11021,6 +11015,20 @@ var MutationBuffer = /*#__PURE__*/ function() {
|
|
|
11021
11015
|
}
|
|
11022
11016
|
}
|
|
11023
11017
|
});
|
|
11018
|
+
__publicField$1(this, "cleanupRemovedNode", function(node2) {
|
|
11019
|
+
if (node2.nodeName === "IFRAME") {
|
|
11020
|
+
try {
|
|
11021
|
+
_this.iframeManager.removeIframe(node2);
|
|
11022
|
+
} catch (e2) {}
|
|
11023
|
+
} else {
|
|
11024
|
+
try {
|
|
11025
|
+
_this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
|
|
11026
|
+
} catch (e2) {}
|
|
11027
|
+
}
|
|
11028
|
+
node2.childNodes.forEach(function(child) {
|
|
11029
|
+
_this.cleanupRemovedNode(child);
|
|
11030
|
+
});
|
|
11031
|
+
});
|
|
11024
11032
|
}
|
|
11025
11033
|
var _proto = MutationBuffer.prototype;
|
|
11026
11034
|
_proto.init = function init(options) {
|
|
@@ -13248,6 +13256,31 @@ var ProcessedNodeManager = /*#__PURE__*/ function() {
|
|
|
13248
13256
|
_proto.destroy = function destroy() {};
|
|
13249
13257
|
return ProcessedNodeManager;
|
|
13250
13258
|
}();
|
|
13259
|
+
function toOrigin(url) {
|
|
13260
|
+
try {
|
|
13261
|
+
var origin = new URL(url).origin;
|
|
13262
|
+
return origin !== "null" ? origin : null;
|
|
13263
|
+
} catch (e) {
|
|
13264
|
+
return null;
|
|
13265
|
+
}
|
|
13266
|
+
}
|
|
13267
|
+
function buildAllowedOriginSet(origins) {
|
|
13268
|
+
if (!Array.isArray(origins) || origins.length === 0) {
|
|
13269
|
+
throw new Error("[rrweb] allowedIframeOrigins must be a non-empty array of origin strings.");
|
|
13270
|
+
}
|
|
13271
|
+
var set = /* @__PURE__ */ new Set();
|
|
13272
|
+
for(var i2 = 0; i2 < origins.length; i2++){
|
|
13273
|
+
var entry = origins[i2];
|
|
13274
|
+
if (typeof entry !== "string") {
|
|
13275
|
+
throw new Error("[rrweb] allowedIframeOrigins[" + i2 + "] must be a string, got " + (typeof entry === "undefined" ? "undefined" : _type_of(entry)) + ".");
|
|
13276
|
+
}
|
|
13277
|
+
var origin = toOrigin(entry);
|
|
13278
|
+
if (origin) {
|
|
13279
|
+
set.add(origin);
|
|
13280
|
+
}
|
|
13281
|
+
}
|
|
13282
|
+
return Object.freeze(set);
|
|
13283
|
+
}
|
|
13251
13284
|
var wrappedEmit;
|
|
13252
13285
|
var takeFullSnapshot$1;
|
|
13253
13286
|
var canvasManager;
|
|
@@ -13269,10 +13302,17 @@ try {
|
|
|
13269
13302
|
var mirror = createMirror$2();
|
|
13270
13303
|
function record(options) {
|
|
13271
13304
|
if (options === void 0) options = {};
|
|
13272
|
-
var emit = options.emit, checkoutEveryNms = options.checkoutEveryNms, checkoutEveryNth = options.checkoutEveryNth, _options_blockClass = options.blockClass, blockClass = _options_blockClass === void 0 ? "rr-block" : _options_blockClass, _options_blockSelector = options.blockSelector, blockSelector = _options_blockSelector === void 0 ? null : _options_blockSelector, _options_ignoreClass = options.ignoreClass, ignoreClass = _options_ignoreClass === void 0 ? "rr-ignore" : _options_ignoreClass, _options_ignoreSelector = options.ignoreSelector, ignoreSelector = _options_ignoreSelector === void 0 ? null : _options_ignoreSelector, _options_maskTextClass = options.maskTextClass, maskTextClass = _options_maskTextClass === void 0 ? "rr-mask" : _options_maskTextClass, _options_maskTextSelector = options.maskTextSelector, maskTextSelector = _options_maskTextSelector === void 0 ? null : _options_maskTextSelector, _options_inlineStylesheet = options.inlineStylesheet, inlineStylesheet = _options_inlineStylesheet === void 0 ? true : _options_inlineStylesheet, maskAllInputs = options.maskAllInputs, _maskInputOptions = options.maskInputOptions, _slimDOMOptions = options.slimDOMOptions, maskInputFn = options.maskInputFn, maskTextFn = options.maskTextFn, hooks = options.hooks, packFn = options.packFn, _options_sampling = options.sampling, sampling = _options_sampling === void 0 ? {} : _options_sampling, _options_dataURLOptions = options.dataURLOptions, dataURLOptions = _options_dataURLOptions === void 0 ? {} : _options_dataURLOptions, mousemoveWait = options.mousemoveWait, _options_recordDOM = options.recordDOM, recordDOM = _options_recordDOM === void 0 ? true : _options_recordDOM, _options_recordCanvas = options.recordCanvas, recordCanvas = _options_recordCanvas === void 0 ? false : _options_recordCanvas, _options_recordCrossOriginIframes = options.recordCrossOriginIframes, recordCrossOriginIframes = _options_recordCrossOriginIframes === void 0 ? false : _options_recordCrossOriginIframes, _options_recordAfter = options.recordAfter, recordAfter = _options_recordAfter === void 0 ? options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load" : _options_recordAfter, _options_userTriggeredOnInput = options.userTriggeredOnInput, userTriggeredOnInput = _options_userTriggeredOnInput === void 0 ? false : _options_userTriggeredOnInput, _options_collectFonts = options.collectFonts, collectFonts = _options_collectFonts === void 0 ? false : _options_collectFonts, _options_inlineImages = options.inlineImages, inlineImages = _options_inlineImages === void 0 ? false : _options_inlineImages, plugins = options.plugins, _options_keepIframeSrcFn = options.keepIframeSrcFn, keepIframeSrcFn = _options_keepIframeSrcFn === void 0 ? function() {
|
|
13305
|
+
var emit = options.emit, checkoutEveryNms = options.checkoutEveryNms, checkoutEveryNth = options.checkoutEveryNth, _options_blockClass = options.blockClass, blockClass = _options_blockClass === void 0 ? "rr-block" : _options_blockClass, _options_blockSelector = options.blockSelector, blockSelector = _options_blockSelector === void 0 ? null : _options_blockSelector, _options_ignoreClass = options.ignoreClass, ignoreClass = _options_ignoreClass === void 0 ? "rr-ignore" : _options_ignoreClass, _options_ignoreSelector = options.ignoreSelector, ignoreSelector = _options_ignoreSelector === void 0 ? null : _options_ignoreSelector, _options_maskTextClass = options.maskTextClass, maskTextClass = _options_maskTextClass === void 0 ? "rr-mask" : _options_maskTextClass, _options_maskTextSelector = options.maskTextSelector, maskTextSelector = _options_maskTextSelector === void 0 ? null : _options_maskTextSelector, _options_inlineStylesheet = options.inlineStylesheet, inlineStylesheet = _options_inlineStylesheet === void 0 ? true : _options_inlineStylesheet, maskAllInputs = options.maskAllInputs, _maskInputOptions = options.maskInputOptions, _slimDOMOptions = options.slimDOMOptions, maskInputFn = options.maskInputFn, maskTextFn = options.maskTextFn, hooks = options.hooks, packFn = options.packFn, _options_sampling = options.sampling, sampling = _options_sampling === void 0 ? {} : _options_sampling, _options_dataURLOptions = options.dataURLOptions, dataURLOptions = _options_dataURLOptions === void 0 ? {} : _options_dataURLOptions, mousemoveWait = options.mousemoveWait, _options_recordDOM = options.recordDOM, recordDOM = _options_recordDOM === void 0 ? true : _options_recordDOM, _options_recordCanvas = options.recordCanvas, recordCanvas = _options_recordCanvas === void 0 ? false : _options_recordCanvas, _options_recordCrossOriginIframes = options.recordCrossOriginIframes, recordCrossOriginIframes = _options_recordCrossOriginIframes === void 0 ? false : _options_recordCrossOriginIframes, allowedIframeOrigins = options.allowedIframeOrigins, _options_recordAfter = options.recordAfter, recordAfter = _options_recordAfter === void 0 ? options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load" : _options_recordAfter, _options_userTriggeredOnInput = options.userTriggeredOnInput, userTriggeredOnInput = _options_userTriggeredOnInput === void 0 ? false : _options_userTriggeredOnInput, _options_collectFonts = options.collectFonts, collectFonts = _options_collectFonts === void 0 ? false : _options_collectFonts, _options_inlineImages = options.inlineImages, inlineImages = _options_inlineImages === void 0 ? false : _options_inlineImages, plugins = options.plugins, _options_keepIframeSrcFn = options.keepIframeSrcFn, keepIframeSrcFn = _options_keepIframeSrcFn === void 0 ? function() {
|
|
13273
13306
|
return false;
|
|
13274
13307
|
} : _options_keepIframeSrcFn, _options_ignoreCSSAttributes = options.ignoreCSSAttributes, ignoreCSSAttributes = _options_ignoreCSSAttributes === void 0 ? /* @__PURE__ */ new Set([]) : _options_ignoreCSSAttributes, errorHandler2 = options.errorHandler;
|
|
13275
13308
|
registerErrorHandler(errorHandler2);
|
|
13309
|
+
var validatedOrigins;
|
|
13310
|
+
if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
|
|
13311
|
+
validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
|
|
13312
|
+
if (validatedOrigins.size === 0) {
|
|
13313
|
+
validatedOrigins = void 0;
|
|
13314
|
+
}
|
|
13315
|
+
}
|
|
13276
13316
|
var inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
|
|
13277
13317
|
var passEmitsToParent = false;
|
|
13278
13318
|
if (!inEmittingFrame) {
|
|
@@ -13364,7 +13404,14 @@ function record(options) {
|
|
|
13364
13404
|
origin: window.location.origin,
|
|
13365
13405
|
isCheckout: isCheckout
|
|
13366
13406
|
};
|
|
13367
|
-
|
|
13407
|
+
if (validatedOrigins) {
|
|
13408
|
+
for(var _iterator = _create_for_of_iterator_helper_loose(validatedOrigins), _step; !(_step = _iterator()).done;){
|
|
13409
|
+
var targetOrigin = _step.value;
|
|
13410
|
+
window.parent.postMessage(message, targetOrigin);
|
|
13411
|
+
}
|
|
13412
|
+
} else {
|
|
13413
|
+
window.parent.postMessage(message, "*");
|
|
13414
|
+
}
|
|
13368
13415
|
}
|
|
13369
13416
|
if (e2.type === EventType.FullSnapshot) {
|
|
13370
13417
|
lastFullSnapshotEvent = e2;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixpanel-browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.78.0",
|
|
4
4
|
"description": "The official Mixpanel JavaScript browser client library",
|
|
5
5
|
"main": "dist/mixpanel.cjs.js",
|
|
6
6
|
"module": "dist/mixpanel.module.js",
|
|
@@ -91,9 +91,10 @@
|
|
|
91
91
|
"webpack": "1.12.2"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@mixpanel/rrweb": "2.0.0-alpha.18.
|
|
95
|
-
"@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.
|
|
96
|
-
"@mixpanel/rrweb-utils": "2.0.0-alpha.18.
|
|
97
|
-
"json-logic-js": "2.0.5"
|
|
94
|
+
"@mixpanel/rrweb": "2.0.0-alpha.18.4",
|
|
95
|
+
"@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.4",
|
|
96
|
+
"@mixpanel/rrweb-utils": "2.0.0-alpha.18.4",
|
|
97
|
+
"json-logic-js": "2.0.5",
|
|
98
|
+
"@types/json-logic-js": "2.0.5"
|
|
98
99
|
}
|
|
99
100
|
}
|
package/src/config.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Flags Module
|
|
2
|
+
|
|
3
|
+
## Testing
|
|
4
|
+
- Test runner is **mocha** (not jest): `BABEL_ENV=test npx mocha --require babel-core/register tests/unit/flags.js`
|
|
5
|
+
- Unit tests live in `tests/unit/flags.js`
|
|
6
|
+
|
|
7
|
+
## Code style
|
|
8
|
+
- ES5 prototypal classes — no arrow functions, no ES6 classes
|
|
9
|
+
- Use `.bind(this)` for `this` context in promise `.then()` / `.catch()` callbacks
|
|
10
|
+
- Flat promise chains preferred over nested `.then()` inside `.then()`
|
|
11
|
+
|
|
12
|
+
## Public API pattern
|
|
13
|
+
- Snake-case aliases are registered at the bottom of `index.js` (e.g., `prototype['load_flags'] = prototype.loadFlags`)
|
|
14
|
+
- New public methods need both the camelCase implementation and a snake_case alias
|
|
15
|
+
|
|
16
|
+
## Error handling convention
|
|
17
|
+
- `fetchFlags()` always rejects on error (single `.catch` that logs and re-throws)
|
|
18
|
+
- Fire-and-forget callers (`init`, `updateContext`, `mixpanel-core.js` identify call) swallow errors at the call site with `.catch(function() {})`
|
|
19
|
+
- User-facing methods like `loadFlags` propagate rejections so the caller can handle them
|
|
20
|
+
|
|
21
|
+
## Key files
|
|
22
|
+
- `src/flags/index.js` — `FeatureFlagManager` class (fetch, load, variants, first-time events)
|
|
23
|
+
- `src/mixpanel-core.js` — calls `fetchFlags()` on distinct_id change (~line 1764)
|
|
24
|
+
- `tests/unit/flags.js` — all flag unit tests
|
package/src/flags/index.js
CHANGED
|
@@ -53,7 +53,9 @@ FeatureFlagManager.prototype.init = function() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
this.flags = null;
|
|
56
|
-
this.fetchFlags()
|
|
56
|
+
this.fetchFlags().catch(function() {
|
|
57
|
+
logger.error('Error fetching flags during init');
|
|
58
|
+
});
|
|
57
59
|
|
|
58
60
|
this.trackedFeatures = new Set();
|
|
59
61
|
this.pendingFirstTimeEvents = {};
|
|
@@ -94,8 +96,12 @@ FeatureFlagManager.prototype.updateContext = function(newContext, options) {
|
|
|
94
96
|
var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
|
|
95
97
|
ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
var configUpdate = {};
|
|
100
|
+
configUpdate[FLAGS_CONFIG_KEY] = ffConfig;
|
|
101
|
+
this.setMpConfig(configUpdate);
|
|
102
|
+
return this.fetchFlags().catch(function() {
|
|
103
|
+
logger.error('Error fetching flags during updateContext');
|
|
104
|
+
});
|
|
99
105
|
};
|
|
100
106
|
|
|
101
107
|
FeatureFlagManager.prototype.areFlagsReady = function() {
|
|
@@ -132,96 +138,110 @@ FeatureFlagManager.prototype.fetchFlags = function() {
|
|
|
132
138
|
}
|
|
133
139
|
}).then(function(response) {
|
|
134
140
|
this.markFetchComplete();
|
|
135
|
-
return response.json()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (hasActivatedEvent) {
|
|
155
|
-
// Preserve the activated variant, don't overwrite with server's current variant
|
|
156
|
-
var currentFlag = this.flags && this.flags.get(key);
|
|
157
|
-
if (currentFlag) {
|
|
158
|
-
flags.set(key, currentFlag);
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
// Use server's current variant
|
|
162
|
-
flags.set(key, {
|
|
163
|
-
'key': data['variant_key'],
|
|
164
|
-
'value': data['variant_value'],
|
|
165
|
-
'experiment_id': data['experiment_id'],
|
|
166
|
-
'is_experiment_active': data['is_experiment_active'],
|
|
167
|
-
'is_qa_tester': data['is_qa_tester']
|
|
168
|
-
});
|
|
141
|
+
return response.json();
|
|
142
|
+
}.bind(this)).then(function(responseBody) {
|
|
143
|
+
var responseFlags = responseBody['flags'];
|
|
144
|
+
if (!responseFlags) {
|
|
145
|
+
throw new Error('No flags in API response');
|
|
146
|
+
}
|
|
147
|
+
var flags = new Map();
|
|
148
|
+
var pendingFirstTimeEvents = {};
|
|
149
|
+
|
|
150
|
+
// Process flags from response
|
|
151
|
+
_.each(responseFlags, function(data, key) {
|
|
152
|
+
// Check if this flag has any activated first-time events this session
|
|
153
|
+
var hasActivatedEvent = false;
|
|
154
|
+
var prefix = key + ':';
|
|
155
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
156
|
+
if (eventKey.startsWith(prefix)) {
|
|
157
|
+
hasActivatedEvent = true;
|
|
169
158
|
}
|
|
170
|
-
}
|
|
159
|
+
});
|
|
171
160
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
'flag_id': def['flag_id'],
|
|
188
|
-
'project_id': def['project_id'],
|
|
189
|
-
'first_time_event_hash': def['first_time_event_hash'],
|
|
190
|
-
'event_name': def['event_name'],
|
|
191
|
-
'property_filters': def['property_filters'],
|
|
192
|
-
'pending_variant': def['pending_variant']
|
|
193
|
-
};
|
|
194
|
-
}, this);
|
|
161
|
+
if (hasActivatedEvent) {
|
|
162
|
+
// Preserve the activated variant, don't overwrite with server's current variant
|
|
163
|
+
var currentFlag = this.flags && this.flags.get(key);
|
|
164
|
+
if (currentFlag) {
|
|
165
|
+
flags.set(key, currentFlag);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
// Use server's current variant
|
|
169
|
+
flags.set(key, {
|
|
170
|
+
'key': data['variant_key'],
|
|
171
|
+
'value': data['variant_value'],
|
|
172
|
+
'experiment_id': data['experiment_id'],
|
|
173
|
+
'is_experiment_active': data['is_experiment_active'],
|
|
174
|
+
'is_qa_tester': data['is_qa_tester']
|
|
175
|
+
});
|
|
195
176
|
}
|
|
177
|
+
}, this);
|
|
178
|
+
|
|
179
|
+
// Process top-level pending_first_time_events array
|
|
180
|
+
var topLevelDefinitions = responseBody['pending_first_time_events'];
|
|
181
|
+
if (topLevelDefinitions && topLevelDefinitions.length > 0) {
|
|
182
|
+
_.each(topLevelDefinitions, function(def) {
|
|
183
|
+
var flagKey = def['flag_key'];
|
|
184
|
+
var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
|
|
185
|
+
|
|
186
|
+
// Skip if this specific event has already been activated this session
|
|
187
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
196
190
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
191
|
+
// Store pending event definition using composite key
|
|
192
|
+
pendingFirstTimeEvents[eventKey] = {
|
|
193
|
+
'flag_key': flagKey,
|
|
194
|
+
'flag_id': def['flag_id'],
|
|
195
|
+
'project_id': def['project_id'],
|
|
196
|
+
'first_time_event_hash': def['first_time_event_hash'],
|
|
197
|
+
'event_name': def['event_name'],
|
|
198
|
+
'property_filters': def['property_filters'],
|
|
199
|
+
'pending_variant': def['pending_variant']
|
|
200
|
+
};
|
|
201
|
+
}, this);
|
|
202
|
+
}
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
this.
|
|
204
|
+
// Preserve any activated orphaned flags (flags that were activated but are no longer in response)
|
|
205
|
+
if (this.activatedFirstTimeEvents) {
|
|
206
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
207
|
+
var flagKey = getFlagKeyFromPendingEventKey(eventKey);
|
|
208
|
+
if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
|
|
209
|
+
// Keep the activated flag even though it's not in the new response
|
|
210
|
+
flags.set(flagKey, this.flags.get(flagKey));
|
|
211
|
+
}
|
|
212
|
+
}, this);
|
|
213
|
+
}
|
|
211
214
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
this.flags = flags;
|
|
216
|
+
this.pendingFirstTimeEvents = pendingFirstTimeEvents;
|
|
217
|
+
this._traceparent = traceparent;
|
|
218
|
+
|
|
219
|
+
this._loadTargetingIfNeeded();
|
|
217
220
|
}.bind(this)).catch(function(error) {
|
|
218
|
-
this.
|
|
221
|
+
if (this._fetchInProgressStartTime) {
|
|
222
|
+
this.markFetchComplete();
|
|
223
|
+
}
|
|
219
224
|
logger.error(error);
|
|
225
|
+
throw error;
|
|
220
226
|
}.bind(this));
|
|
221
227
|
|
|
222
228
|
return this.fetchPromise;
|
|
223
229
|
};
|
|
224
230
|
|
|
231
|
+
FeatureFlagManager.prototype.loadFlags = function() {
|
|
232
|
+
if (!this.isSystemEnabled()) {
|
|
233
|
+
return Promise.resolve();
|
|
234
|
+
}
|
|
235
|
+
if (!this.trackedFeatures) {
|
|
236
|
+
logger.error('loadFlags called before init');
|
|
237
|
+
return Promise.resolve();
|
|
238
|
+
}
|
|
239
|
+
if (this._fetchInProgressStartTime) {
|
|
240
|
+
return this.fetchPromise;
|
|
241
|
+
}
|
|
242
|
+
return this.fetchFlags();
|
|
243
|
+
};
|
|
244
|
+
|
|
225
245
|
FeatureFlagManager.prototype.markFetchComplete = function() {
|
|
226
246
|
if (!this._fetchInProgressStartTime) {
|
|
227
247
|
logger.error('Fetch in progress started time not set, cannot mark fetch complete');
|
|
@@ -501,6 +521,13 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
|
|
|
501
521
|
this.track('$experiment_started', trackingProperties);
|
|
502
522
|
};
|
|
503
523
|
|
|
524
|
+
FeatureFlagManager.prototype.whenReady = function() {
|
|
525
|
+
if (this.fetchPromise) {
|
|
526
|
+
return this.fetchPromise;
|
|
527
|
+
}
|
|
528
|
+
return Promise.resolve();
|
|
529
|
+
};
|
|
530
|
+
|
|
504
531
|
FeatureFlagManager.prototype.minApisSupported = function() {
|
|
505
532
|
return !!this.fetch &&
|
|
506
533
|
typeof Promise !== 'undefined' &&
|
|
@@ -517,7 +544,9 @@ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype
|
|
|
517
544
|
FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
|
|
518
545
|
FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
|
|
519
546
|
FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
|
|
547
|
+
FeatureFlagManager.prototype['load_flags'] = FeatureFlagManager.prototype.loadFlags;
|
|
520
548
|
FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
|
|
549
|
+
FeatureFlagManager.prototype['when_ready'] = FeatureFlagManager.prototype.whenReady;
|
|
521
550
|
|
|
522
551
|
// Deprecated method
|
|
523
552
|
FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
|
package/src/index.d.ts
CHANGED
|
@@ -252,11 +252,12 @@ export interface Config {
|
|
|
252
252
|
record_mask_all_inputs: boolean;
|
|
253
253
|
record_min_ms: number;
|
|
254
254
|
record_max_ms: number;
|
|
255
|
-
|
|
255
|
+
record_allowed_iframe_origins: string[];
|
|
256
256
|
record_canvas: boolean;
|
|
257
257
|
recording_event_triggers: RecordingEventTriggers;
|
|
258
258
|
record_heatmap_data: boolean;
|
|
259
259
|
remote_settings_mode: RemoteSettingType;
|
|
260
|
+
record_sessions_percent: number;
|
|
260
261
|
hooks: {
|
|
261
262
|
before_identify?: (new_distinct_id: string) => string | null;
|
|
262
263
|
before_register?: (
|
|
@@ -356,6 +357,7 @@ export interface FlagsUpdateContextOptions {
|
|
|
356
357
|
|
|
357
358
|
export interface FlagsManager {
|
|
358
359
|
are_flags_ready(): boolean;
|
|
360
|
+
load_flags(): Promise<void>;
|
|
359
361
|
get_variant(
|
|
360
362
|
featureName: string,
|
|
361
363
|
fallback: FlagsVariant
|
package/src/mixpanel-core.js
CHANGED
|
@@ -64,7 +64,6 @@ var INIT_SNIPPET = 1;
|
|
|
64
64
|
/** @const */ var SETTING_FALLBACK = 'fallback';
|
|
65
65
|
/** @const */ var SETTING_DISABLED = 'disabled';
|
|
66
66
|
|
|
67
|
-
|
|
68
67
|
/*
|
|
69
68
|
* Dynamic... constants? Is that an oxymoron?
|
|
70
69
|
*/
|
|
@@ -149,6 +148,7 @@ var DEFAULT_CONFIG = {
|
|
|
149
148
|
'batch_request_timeout_ms': 90000,
|
|
150
149
|
'batch_autostart': true,
|
|
151
150
|
'hooks': {},
|
|
151
|
+
'record_allowed_iframe_origins': [],
|
|
152
152
|
'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
|
|
153
153
|
'record_block_selector': 'img, video, audio',
|
|
154
154
|
'record_canvas': false,
|
|
@@ -1724,7 +1724,9 @@ MixpanelLib.prototype.identify = function(
|
|
|
1724
1724
|
|
|
1725
1725
|
// check feature flags again if distinct id has changed
|
|
1726
1726
|
if (new_distinct_id !== previous_distinct_id) {
|
|
1727
|
-
this.flags.fetchFlags()
|
|
1727
|
+
this.flags.fetchFlags().catch(function() {
|
|
1728
|
+
console.error('[flags] Error fetching flags during identify');
|
|
1729
|
+
});
|
|
1728
1730
|
}
|
|
1729
1731
|
};
|
|
1730
1732
|
|
|
@@ -10,7 +10,7 @@ import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
|
|
|
10
10
|
import { RequestBatcher } from '../request-batcher';
|
|
11
11
|
|
|
12
12
|
import { Config } from '../config';
|
|
13
|
-
import { RECORD_ENQUEUE_THROTTLE_MS } from './utils';
|
|
13
|
+
import { RECORD_ENQUEUE_THROTTLE_MS, validateAllowedOrigins } from './utils';
|
|
14
14
|
import { shouldMaskInput, shouldMaskText, getPrivacyConfig } from './masking';
|
|
15
15
|
import { getRecordNetworkPlugin } from './rrweb-network-plugin';
|
|
16
16
|
|
|
@@ -265,6 +265,8 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
var validatedOrigins = validateAllowedOrigins(this.getConfig('record_allowed_iframe_origins'), logger);
|
|
269
|
+
|
|
268
270
|
try {
|
|
269
271
|
this._stopRecording = this._rrwebRecord({
|
|
270
272
|
'emit': function (ev) {
|
|
@@ -299,6 +301,8 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
|
|
|
299
301
|
'maskTextSelector': '*',
|
|
300
302
|
'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
|
|
301
303
|
'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
|
|
304
|
+
'recordCrossOriginIframes': validatedOrigins.length > 0,
|
|
305
|
+
'allowedIframeOrigins': validatedOrigins,
|
|
302
306
|
'recordCanvas': this.getConfig('record_canvas'),
|
|
303
307
|
'sampling': {
|
|
304
308
|
'canvas': 15
|
package/src/recorder/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { _ } from '../utils';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @param {import('./session-recording').SerializedRecording} serializedRecording
|
|
3
5
|
* @returns {boolean}
|
|
@@ -10,7 +12,31 @@ var isRecordingExpired = function(serializedRecording) {
|
|
|
10
12
|
|
|
11
13
|
var RECORD_ENQUEUE_THROTTLE_MS = 250;
|
|
12
14
|
|
|
15
|
+
var validateAllowedOrigins = function(origins, logger) {
|
|
16
|
+
if (!_.isArray(origins)) {
|
|
17
|
+
if (origins) {
|
|
18
|
+
logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
var valid = [];
|
|
23
|
+
for (var i = 0; i < origins.length; i++) {
|
|
24
|
+
try {
|
|
25
|
+
var origin = new URL(origins[i]).origin;
|
|
26
|
+
if (origin === 'null') {
|
|
27
|
+
logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
valid.push(origin);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return valid;
|
|
36
|
+
};
|
|
37
|
+
|
|
13
38
|
export {
|
|
14
39
|
isRecordingExpired,
|
|
15
|
-
RECORD_ENQUEUE_THROTTLE_MS
|
|
40
|
+
RECORD_ENQUEUE_THROTTLE_MS,
|
|
41
|
+
validateAllowedOrigins
|
|
16
42
|
};
|