mixpanel-browser 2.74.0 → 2.75.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/.github/workflows/unit-tests.yml +1 -1
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/dist/mixpanel-core.cjs.js +318 -20
- package/dist/mixpanel-recorder.js +127 -15
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +2576 -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 +522 -0
- package/dist/mixpanel-with-async-modules.cjs.js +9700 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +318 -20
- package/dist/mixpanel-with-recorder.js +435 -26
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.js +1020 -28
- package/dist/mixpanel.cjs.js +1020 -28
- package/dist/mixpanel.globals.js +318 -20
- package/dist/mixpanel.min.js +179 -172
- package/dist/mixpanel.module.js +1020 -28
- package/dist/mixpanel.umd.js +1020 -28
- package/dist/rrweb-bundled.js +119 -5
- package/dist/rrweb-compiled.js +116 -5
- package/package.json +4 -3
- package/rollup.config.mjs +34 -2
- package/src/config.js +1 -1
- package/src/flags/index.js +269 -8
- package/src/globals.js +14 -0
- package/src/loaders/loader-module.js +1 -0
- package/src/mixpanel-core.js +12 -3
- package/src/recorder/index.js +2 -1
- package/src/targeting/event-matcher.js +97 -0
- package/src/targeting/index.js +11 -0
- package/src/targeting/loader.js +36 -0
- package/src/utils.js +1 -8
- package/.claude/settings.local.json +0 -12
- /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
|
@@ -26,6 +26,16 @@
|
|
|
26
26
|
win = window;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Shared global window property names used across modules
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Targeting library global (used by flags and targeting modules)
|
|
34
|
+
var TARGETING_GLOBAL_NAME = '__mp_targeting';
|
|
35
|
+
|
|
36
|
+
// Recorder library global (used by recorder and mixpanel-core)
|
|
37
|
+
var RECORDER_GLOBAL_NAME = '__mp_recorder';
|
|
38
|
+
|
|
29
39
|
function _array_like_to_array(arr, len) {
|
|
30
40
|
if (len == null || len > arr.length) len = arr.length;
|
|
31
41
|
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
|
|
@@ -640,14 +650,16 @@
|
|
|
640
650
|
return this.nodeMetaMap.get(n2) || null;
|
|
641
651
|
};
|
|
642
652
|
// removes the node from idNodeMap
|
|
643
|
-
//
|
|
644
|
-
_proto.removeNodeFromMap = function removeNodeFromMap(n2) {
|
|
653
|
+
// if permanent is true, also removes from nodeMetaMap
|
|
654
|
+
_proto.removeNodeFromMap = function removeNodeFromMap(n2, permanent) {
|
|
645
655
|
var _this = this;
|
|
656
|
+
if (permanent === void 0) permanent = false;
|
|
646
657
|
var id = this.getId(n2);
|
|
647
658
|
this.idNodeMap.delete(id);
|
|
659
|
+
if (permanent) this.nodeMetaMap.delete(n2);
|
|
648
660
|
if (n2.childNodes) {
|
|
649
661
|
n2.childNodes.forEach(function(childNode) {
|
|
650
|
-
return _this.removeNodeFromMap(childNode);
|
|
662
|
+
return _this.removeNodeFromMap(childNode, permanent);
|
|
651
663
|
});
|
|
652
664
|
}
|
|
653
665
|
};
|
|
@@ -10389,6 +10401,15 @@
|
|
|
10389
10401
|
_proto.generateId = function generateId() {
|
|
10390
10402
|
return this.id++;
|
|
10391
10403
|
};
|
|
10404
|
+
_proto.remove = function remove(stylesheet) {
|
|
10405
|
+
var id = this.styleIDMap.get(stylesheet);
|
|
10406
|
+
if (id !== void 0) {
|
|
10407
|
+
this.styleIDMap.delete(stylesheet);
|
|
10408
|
+
this.idStyleMap.delete(id);
|
|
10409
|
+
return true;
|
|
10410
|
+
}
|
|
10411
|
+
return false;
|
|
10412
|
+
};
|
|
10392
10413
|
return StyleSheetMirror;
|
|
10393
10414
|
}();
|
|
10394
10415
|
function getShadowHost(n2) {
|
|
@@ -10711,7 +10732,15 @@
|
|
|
10711
10732
|
}
|
|
10712
10733
|
};
|
|
10713
10734
|
while(_this.mapRemoves.length){
|
|
10714
|
-
_this.
|
|
10735
|
+
var removedNode = _this.mapRemoves.shift();
|
|
10736
|
+
if (removedNode.nodeName === "IFRAME") {
|
|
10737
|
+
try {
|
|
10738
|
+
_this.iframeManager.removeIframe(removedNode);
|
|
10739
|
+
} catch (e2) {}
|
|
10740
|
+
} else {
|
|
10741
|
+
_this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
|
|
10742
|
+
}
|
|
10743
|
+
_this.mirror.removeNodeFromMap(removedNode);
|
|
10715
10744
|
}
|
|
10716
10745
|
for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
|
|
10717
10746
|
var n2 = _step.value;
|
|
@@ -11085,6 +11114,9 @@
|
|
|
11085
11114
|
this.shadowDomManager.reset();
|
|
11086
11115
|
this.canvasManager.reset();
|
|
11087
11116
|
};
|
|
11117
|
+
_proto.getDoc = function getDoc() {
|
|
11118
|
+
return this.doc;
|
|
11119
|
+
};
|
|
11088
11120
|
return MutationBuffer;
|
|
11089
11121
|
}();
|
|
11090
11122
|
function deepDelete(addsSet, n2) {
|
|
@@ -11185,6 +11217,14 @@
|
|
|
11185
11217
|
});
|
|
11186
11218
|
return observer;
|
|
11187
11219
|
}
|
|
11220
|
+
function removeMutationBufferForDoc(doc) {
|
|
11221
|
+
for(var i2 = mutationBuffers.length - 1; i2 >= 0; i2--){
|
|
11222
|
+
var buffer = mutationBuffers[i2];
|
|
11223
|
+
if (buffer.getDoc() === doc) {
|
|
11224
|
+
mutationBuffers.splice(i2, 1);
|
|
11225
|
+
}
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11188
11228
|
function initMoveObserver(param) {
|
|
11189
11229
|
var mousemoveCb = param.mousemoveCb, sampling = param.sampling, doc = param.doc, mirror2 = param.mirror;
|
|
11190
11230
|
if (sampling.mousemove === false) {
|
|
@@ -12200,6 +12240,8 @@
|
|
|
12200
12240
|
__publicField$1(this, "crossOriginIframeMirror", new CrossOriginIframeMirror(genId));
|
|
12201
12241
|
__publicField$1(this, "crossOriginIframeStyleMirror");
|
|
12202
12242
|
__publicField$1(this, "crossOriginIframeRootIdMap", /* @__PURE__ */ new WeakMap());
|
|
12243
|
+
__publicField$1(this, "iframeContentDocumentMap", /* @__PURE__ */ new WeakMap());
|
|
12244
|
+
__publicField$1(this, "iframeObserverCleanupMap", /* @__PURE__ */ new WeakMap());
|
|
12203
12245
|
__publicField$1(this, "mirror");
|
|
12204
12246
|
__publicField$1(this, "mutationCb");
|
|
12205
12247
|
__publicField$1(this, "wrappedEmit");
|
|
@@ -12221,6 +12263,31 @@
|
|
|
12221
12263
|
this.iframes.set(iframeEl, true);
|
|
12222
12264
|
if (iframeEl.contentWindow) this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
|
|
12223
12265
|
};
|
|
12266
|
+
_proto.getIframeContentDocument = function getIframeContentDocument(iframeEl) {
|
|
12267
|
+
return this.iframeContentDocumentMap.get(iframeEl);
|
|
12268
|
+
};
|
|
12269
|
+
_proto.setObserverCleanup = function setObserverCleanup(iframeEl, cleanup) {
|
|
12270
|
+
this.iframeObserverCleanupMap.set(iframeEl, cleanup);
|
|
12271
|
+
};
|
|
12272
|
+
_proto.getObserverCleanup = function getObserverCleanup(iframeEl) {
|
|
12273
|
+
return this.iframeObserverCleanupMap.get(iframeEl);
|
|
12274
|
+
};
|
|
12275
|
+
_proto.removeIframe = function removeIframe(iframeEl) {
|
|
12276
|
+
var storedDoc = this.iframeContentDocumentMap.get(iframeEl);
|
|
12277
|
+
if (storedDoc) {
|
|
12278
|
+
this.stylesheetManager.cleanupStylesheetsForRemovedNode(storedDoc);
|
|
12279
|
+
this.mirror.removeNodeFromMap(storedDoc, true);
|
|
12280
|
+
}
|
|
12281
|
+
this.iframes.delete(iframeEl);
|
|
12282
|
+
this.iframeContentDocumentMap.delete(iframeEl);
|
|
12283
|
+
var observerCleanup = this.iframeObserverCleanupMap.get(iframeEl);
|
|
12284
|
+
if (observerCleanup) {
|
|
12285
|
+
try {
|
|
12286
|
+
observerCleanup();
|
|
12287
|
+
} catch (e2) {}
|
|
12288
|
+
this.iframeObserverCleanupMap.delete(iframeEl);
|
|
12289
|
+
}
|
|
12290
|
+
};
|
|
12224
12291
|
_proto.addLoadListener = function addLoadListener(cb) {
|
|
12225
12292
|
this.loadListener = cb;
|
|
12226
12293
|
};
|
|
@@ -12239,6 +12306,9 @@
|
|
|
12239
12306
|
attributes: [],
|
|
12240
12307
|
isAttachIframe: true
|
|
12241
12308
|
});
|
|
12309
|
+
if (iframeEl.contentDocument) {
|
|
12310
|
+
this.iframeContentDocumentMap.set(iframeEl, iframeEl.contentDocument);
|
|
12311
|
+
}
|
|
12242
12312
|
if (this.recordCrossOriginIframes) (_a2 = iframeEl.contentWindow) == null ? void 0 : _a2.addEventListener("message", this.handleMessage.bind(this));
|
|
12243
12313
|
(_b = this.loadListener) == null ? void 0 : _b.call(this, iframeEl);
|
|
12244
12314
|
if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets && iframeEl.contentDocument.adoptedStyleSheets.length > 0) this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
|
|
@@ -13151,6 +13221,41 @@
|
|
|
13151
13221
|
this.styleMirror.reset();
|
|
13152
13222
|
this.trackedLinkElements = /* @__PURE__ */ new WeakSet();
|
|
13153
13223
|
};
|
|
13224
|
+
/**
|
|
13225
|
+
* Cleans up stylesheets associated with a removed node.
|
|
13226
|
+
*
|
|
13227
|
+
* @param removedNode - The node that was removed from the DOM.
|
|
13228
|
+
*/ _proto.cleanupStylesheetsForRemovedNode = function cleanupStylesheetsForRemovedNode(removedNode) {
|
|
13229
|
+
var _this = this;
|
|
13230
|
+
try {
|
|
13231
|
+
if (removedNode.nodeType === Node.DOCUMENT_NODE) {
|
|
13232
|
+
var doc = removedNode;
|
|
13233
|
+
if (doc.adoptedStyleSheets) {
|
|
13234
|
+
for(var _iterator = _create_for_of_iterator_helper_loose(doc.adoptedStyleSheets), _step; !(_step = _iterator()).done;){
|
|
13235
|
+
var sheet = _step.value;
|
|
13236
|
+
this.styleMirror.remove(sheet);
|
|
13237
|
+
}
|
|
13238
|
+
}
|
|
13239
|
+
}
|
|
13240
|
+
if (removedNode.nodeName === "STYLE") {
|
|
13241
|
+
var styleEl = removedNode;
|
|
13242
|
+
if (styleEl.sheet) {
|
|
13243
|
+
this.styleMirror.remove(styleEl.sheet);
|
|
13244
|
+
}
|
|
13245
|
+
}
|
|
13246
|
+
if (removedNode.nodeName === "LINK" && removedNode.rel === "stylesheet") {
|
|
13247
|
+
var linkEl = removedNode;
|
|
13248
|
+
if (linkEl.sheet) {
|
|
13249
|
+
this.styleMirror.remove(linkEl.sheet);
|
|
13250
|
+
}
|
|
13251
|
+
}
|
|
13252
|
+
if (removedNode.childNodes) {
|
|
13253
|
+
removedNode.childNodes.forEach(function(child) {
|
|
13254
|
+
_this.cleanupStylesheetsForRemovedNode(child);
|
|
13255
|
+
});
|
|
13256
|
+
}
|
|
13257
|
+
} catch (e2) {}
|
|
13258
|
+
};
|
|
13154
13259
|
// TODO: take snapshot on stylesheet reload by applying event listener
|
|
13155
13260
|
_proto.trackStylesheetInLinkElement = function trackStylesheetInLinkElement(_linkEl) {};
|
|
13156
13261
|
return StylesheetManager;
|
|
@@ -13605,7 +13710,23 @@
|
|
|
13605
13710
|
};
|
|
13606
13711
|
iframeManager.addLoadListener(function(iframeEl) {
|
|
13607
13712
|
try {
|
|
13608
|
-
|
|
13713
|
+
var iframeDoc = iframeEl.contentDocument;
|
|
13714
|
+
var iframeHandler = observe(iframeDoc);
|
|
13715
|
+
handlers.push(iframeHandler);
|
|
13716
|
+
var existingCleanup = iframeManager.getObserverCleanup(iframeEl);
|
|
13717
|
+
iframeManager.setObserverCleanup(iframeEl, function() {
|
|
13718
|
+
if (existingCleanup) {
|
|
13719
|
+
try {
|
|
13720
|
+
existingCleanup();
|
|
13721
|
+
} catch (e2) {}
|
|
13722
|
+
}
|
|
13723
|
+
try {
|
|
13724
|
+
iframeHandler();
|
|
13725
|
+
var idx = handlers.indexOf(iframeHandler);
|
|
13726
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
13727
|
+
removeMutationBufferForDoc(iframeDoc);
|
|
13728
|
+
} catch (e2) {}
|
|
13729
|
+
});
|
|
13609
13730
|
} catch (error) {
|
|
13610
13731
|
console.warn(error);
|
|
13611
13732
|
}
|
|
@@ -18854,7 +18975,7 @@
|
|
|
18854
18975
|
|
|
18855
18976
|
var Config = {
|
|
18856
18977
|
DEBUG: false,
|
|
18857
|
-
LIB_VERSION: '2.
|
|
18978
|
+
LIB_VERSION: '2.75.0'
|
|
18858
18979
|
};
|
|
18859
18980
|
|
|
18860
18981
|
/* eslint camelcase: "off", eqeqeq: "off" */
|
|
@@ -19060,15 +19181,8 @@
|
|
|
19060
19181
|
return toString.call(obj) === '[object Array]';
|
|
19061
19182
|
};
|
|
19062
19183
|
|
|
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
19184
|
_.isFunction = function(f) {
|
|
19067
|
-
|
|
19068
|
-
return /^\s*\bfunction\b/.test(f);
|
|
19069
|
-
} catch (x) {
|
|
19070
|
-
return false;
|
|
19071
|
-
}
|
|
19185
|
+
return typeof f === 'function';
|
|
19072
19186
|
};
|
|
19073
19187
|
|
|
19074
19188
|
_.isArguments = function(obj) {
|
|
@@ -23762,7 +23876,7 @@
|
|
|
23762
23876
|
}
|
|
23763
23877
|
});
|
|
23764
23878
|
|
|
23765
|
-
win[
|
|
23879
|
+
win[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
|
|
23766
23880
|
|
|
23767
23881
|
/** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
|
|
23768
23882
|
/** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
|
|
@@ -24733,14 +24847,62 @@
|
|
|
24733
24847
|
// TODO integrate error_reporter from mixpanel instance
|
|
24734
24848
|
safewrapClass(Autocapture);
|
|
24735
24849
|
|
|
24736
|
-
|
|
24850
|
+
/**
|
|
24851
|
+
* Get the promise-based targeting loader
|
|
24852
|
+
* @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
|
|
24853
|
+
* @param {string} targetingSrc - URL to targeting bundle
|
|
24854
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
24855
|
+
*/
|
|
24856
|
+
var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
|
|
24857
|
+
// Return existing promise if already initialized or loading
|
|
24858
|
+
if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
|
|
24859
|
+
return win[TARGETING_GLOBAL_NAME];
|
|
24860
|
+
}
|
|
24861
|
+
|
|
24862
|
+
// Create loading promise and set it as the global immediately
|
|
24863
|
+
// This makes minified build behavior consistent with dev/CJS builds
|
|
24864
|
+
win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
|
|
24865
|
+
loadExtraBundle(targetingSrc, resolve);
|
|
24866
|
+
}).then(function () {
|
|
24867
|
+
var p = win[TARGETING_GLOBAL_NAME];
|
|
24868
|
+
if (p && typeof p.then === 'function') {
|
|
24869
|
+
return p;
|
|
24870
|
+
}
|
|
24871
|
+
throw new Error('targeting failed to load');
|
|
24872
|
+
}).catch(function (err) {
|
|
24873
|
+
delete win[TARGETING_GLOBAL_NAME];
|
|
24874
|
+
throw err;
|
|
24875
|
+
});
|
|
24876
|
+
|
|
24877
|
+
return win[TARGETING_GLOBAL_NAME];
|
|
24878
|
+
};
|
|
24737
24879
|
|
|
24880
|
+
var logger = console_with_prefix('flags');
|
|
24738
24881
|
var FLAGS_CONFIG_KEY = 'flags';
|
|
24739
24882
|
|
|
24740
24883
|
var CONFIG_CONTEXT = 'context';
|
|
24741
24884
|
var CONFIG_DEFAULTS = {};
|
|
24742
24885
|
CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
|
|
24743
24886
|
|
|
24887
|
+
/**
|
|
24888
|
+
* Generate a unique key for a pending first-time event
|
|
24889
|
+
* @param {string} flagKey - The flag key
|
|
24890
|
+
* @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
|
|
24891
|
+
* @returns {string} Composite key in format "flagKey:firstTimeEventHash"
|
|
24892
|
+
*/
|
|
24893
|
+
var getPendingEventKey = function(flagKey, firstTimeEventHash) {
|
|
24894
|
+
return flagKey + ':' + firstTimeEventHash;
|
|
24895
|
+
};
|
|
24896
|
+
|
|
24897
|
+
/**
|
|
24898
|
+
* Extract the flag key from a pending event key
|
|
24899
|
+
* @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
|
|
24900
|
+
* @returns {string} The flag key portion
|
|
24901
|
+
*/
|
|
24902
|
+
var getFlagKeyFromPendingEventKey = function(eventKey) {
|
|
24903
|
+
return eventKey.split(':')[0];
|
|
24904
|
+
};
|
|
24905
|
+
|
|
24744
24906
|
/**
|
|
24745
24907
|
* FeatureFlagManager: support for Mixpanel's feature flagging product
|
|
24746
24908
|
* @constructor
|
|
@@ -24752,6 +24914,8 @@
|
|
|
24752
24914
|
this.setMpConfig = initOptions.setConfigFunc;
|
|
24753
24915
|
this.getMpProperty = initOptions.getPropertyFunc;
|
|
24754
24916
|
this.track = initOptions.trackingFunc;
|
|
24917
|
+
this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
|
|
24918
|
+
this.targetingSrc = initOptions.targetingSrc || '';
|
|
24755
24919
|
};
|
|
24756
24920
|
|
|
24757
24921
|
FeatureFlagManager.prototype.init = function() {
|
|
@@ -24764,6 +24928,8 @@
|
|
|
24764
24928
|
this.fetchFlags();
|
|
24765
24929
|
|
|
24766
24930
|
this.trackedFeatures = new Set();
|
|
24931
|
+
this.pendingFirstTimeEvents = {};
|
|
24932
|
+
this.activatedFirstTimeEvents = {};
|
|
24767
24933
|
};
|
|
24768
24934
|
|
|
24769
24935
|
FeatureFlagManager.prototype.getFullConfig = function() {
|
|
@@ -24844,17 +25010,78 @@
|
|
|
24844
25010
|
throw new Error('No flags in API response');
|
|
24845
25011
|
}
|
|
24846
25012
|
var flags = new Map();
|
|
25013
|
+
var pendingFirstTimeEvents = {};
|
|
25014
|
+
|
|
25015
|
+
// Process flags from response
|
|
24847
25016
|
_.each(responseFlags, function(data, key) {
|
|
24848
|
-
|
|
24849
|
-
|
|
24850
|
-
|
|
24851
|
-
|
|
24852
|
-
|
|
24853
|
-
|
|
25017
|
+
// Check if this flag has any activated first-time events this session
|
|
25018
|
+
var hasActivatedEvent = false;
|
|
25019
|
+
var prefix = key + ':';
|
|
25020
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
25021
|
+
if (eventKey.startsWith(prefix)) {
|
|
25022
|
+
hasActivatedEvent = true;
|
|
25023
|
+
}
|
|
24854
25024
|
});
|
|
24855
|
-
|
|
25025
|
+
|
|
25026
|
+
if (hasActivatedEvent) {
|
|
25027
|
+
// Preserve the activated variant, don't overwrite with server's current variant
|
|
25028
|
+
var currentFlag = this.flags && this.flags.get(key);
|
|
25029
|
+
if (currentFlag) {
|
|
25030
|
+
flags.set(key, currentFlag);
|
|
25031
|
+
}
|
|
25032
|
+
} else {
|
|
25033
|
+
// Use server's current variant
|
|
25034
|
+
flags.set(key, {
|
|
25035
|
+
'key': data['variant_key'],
|
|
25036
|
+
'value': data['variant_value'],
|
|
25037
|
+
'experiment_id': data['experiment_id'],
|
|
25038
|
+
'is_experiment_active': data['is_experiment_active'],
|
|
25039
|
+
'is_qa_tester': data['is_qa_tester']
|
|
25040
|
+
});
|
|
25041
|
+
}
|
|
25042
|
+
}, this);
|
|
25043
|
+
|
|
25044
|
+
// Process top-level pending_first_time_events array
|
|
25045
|
+
var topLevelDefinitions = responseBody['pending_first_time_events'];
|
|
25046
|
+
if (topLevelDefinitions && topLevelDefinitions.length > 0) {
|
|
25047
|
+
_.each(topLevelDefinitions, function(def) {
|
|
25048
|
+
var flagKey = def['flag_key'];
|
|
25049
|
+
var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
|
|
25050
|
+
|
|
25051
|
+
// Skip if this specific event has already been activated this session
|
|
25052
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
25053
|
+
return;
|
|
25054
|
+
}
|
|
25055
|
+
|
|
25056
|
+
// Store pending event definition using composite key
|
|
25057
|
+
pendingFirstTimeEvents[eventKey] = {
|
|
25058
|
+
'flag_key': flagKey,
|
|
25059
|
+
'flag_id': def['flag_id'],
|
|
25060
|
+
'project_id': def['project_id'],
|
|
25061
|
+
'first_time_event_hash': def['first_time_event_hash'],
|
|
25062
|
+
'event_name': def['event_name'],
|
|
25063
|
+
'property_filters': def['property_filters'],
|
|
25064
|
+
'pending_variant': def['pending_variant']
|
|
25065
|
+
};
|
|
25066
|
+
}, this);
|
|
25067
|
+
}
|
|
25068
|
+
|
|
25069
|
+
// Preserve any activated orphaned flags (flags that were activated but are no longer in response)
|
|
25070
|
+
if (this.activatedFirstTimeEvents) {
|
|
25071
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
25072
|
+
var flagKey = getFlagKeyFromPendingEventKey(eventKey);
|
|
25073
|
+
if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
|
|
25074
|
+
// Keep the activated flag even though it's not in the new response
|
|
25075
|
+
flags.set(flagKey, this.flags.get(flagKey));
|
|
25076
|
+
}
|
|
25077
|
+
}, this);
|
|
25078
|
+
}
|
|
25079
|
+
|
|
24856
25080
|
this.flags = flags;
|
|
25081
|
+
this.pendingFirstTimeEvents = pendingFirstTimeEvents;
|
|
24857
25082
|
this._traceparent = traceparent;
|
|
25083
|
+
|
|
25084
|
+
this._loadTargetingIfNeeded();
|
|
24858
25085
|
}.bind(this)).catch(function(error) {
|
|
24859
25086
|
this.markFetchComplete();
|
|
24860
25087
|
logger.error(error);
|
|
@@ -24878,6 +25105,177 @@
|
|
|
24878
25105
|
this._fetchInProgressStartTime = null;
|
|
24879
25106
|
};
|
|
24880
25107
|
|
|
25108
|
+
/**
|
|
25109
|
+
* Proactively load targeting bundle if any pending events have property filters
|
|
25110
|
+
*/
|
|
25111
|
+
FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
|
|
25112
|
+
var hasPropertyFilters = false;
|
|
25113
|
+
_.each(this.pendingFirstTimeEvents, function(evt) {
|
|
25114
|
+
if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
|
|
25115
|
+
hasPropertyFilters = true;
|
|
25116
|
+
}
|
|
25117
|
+
});
|
|
25118
|
+
|
|
25119
|
+
if (hasPropertyFilters) {
|
|
25120
|
+
this.getTargeting().then(function() {
|
|
25121
|
+
logger.log('targeting loaded for property filter evaluation');
|
|
25122
|
+
});
|
|
25123
|
+
}
|
|
25124
|
+
};
|
|
25125
|
+
|
|
25126
|
+
/**
|
|
25127
|
+
* Get the targeting library (initializes if not already loaded)
|
|
25128
|
+
* This method is primarily for testing - production code should rely on automatic loading
|
|
25129
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
25130
|
+
*/
|
|
25131
|
+
FeatureFlagManager.prototype.getTargeting = function() {
|
|
25132
|
+
return getTargetingPromise(
|
|
25133
|
+
this.loadExtraBundle.bind(this),
|
|
25134
|
+
this.targetingSrc
|
|
25135
|
+
).catch(function(error) {
|
|
25136
|
+
logger.error('Failed to load targeting: ' + error);
|
|
25137
|
+
}.bind(this));
|
|
25138
|
+
};
|
|
25139
|
+
|
|
25140
|
+
/**
|
|
25141
|
+
* Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
|
|
25142
|
+
* @param {string} eventName - The name of the event being tracked
|
|
25143
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
25144
|
+
*
|
|
25145
|
+
* When a match is found (event name matches and property filters pass), this method:
|
|
25146
|
+
* - Switches the flag to the pending variant
|
|
25147
|
+
* - Marks the event as activated for this session
|
|
25148
|
+
* - Records the activation via the API (fire-and-forget)
|
|
25149
|
+
*/
|
|
25150
|
+
FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
|
|
25151
|
+
if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
|
|
25152
|
+
return;
|
|
25153
|
+
}
|
|
25154
|
+
|
|
25155
|
+
// Check if targeting promise exists (either bundled or async loaded)
|
|
25156
|
+
if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
|
|
25157
|
+
win[TARGETING_GLOBAL_NAME].then(function(library) {
|
|
25158
|
+
this._processFirstTimeEventCheck(eventName, properties, library);
|
|
25159
|
+
}.bind(this)).catch(function() {
|
|
25160
|
+
// If targeting failed to load, process with null
|
|
25161
|
+
// Events without property filters will still match
|
|
25162
|
+
this._processFirstTimeEventCheck(eventName, properties, null);
|
|
25163
|
+
}.bind(this));
|
|
25164
|
+
} else {
|
|
25165
|
+
// No targeting available, process with null
|
|
25166
|
+
// Events without property filters will still match
|
|
25167
|
+
this._processFirstTimeEventCheck(eventName, properties, null);
|
|
25168
|
+
}
|
|
25169
|
+
};
|
|
25170
|
+
|
|
25171
|
+
/**
|
|
25172
|
+
* Internal method to process first-time event checks with loaded targeting library
|
|
25173
|
+
* @param {string} eventName - The name of the event being tracked
|
|
25174
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
25175
|
+
* @param {Object} targeting - The loaded targeting library
|
|
25176
|
+
*/
|
|
25177
|
+
FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
|
|
25178
|
+
_.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
|
|
25179
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
25180
|
+
return;
|
|
25181
|
+
}
|
|
25182
|
+
|
|
25183
|
+
var flagKey = pendingEvent['flag_key'];
|
|
25184
|
+
|
|
25185
|
+
// Use targeting module to check if event matches
|
|
25186
|
+
var matchResult;
|
|
25187
|
+
|
|
25188
|
+
// If no targeting library and event has property filters, skip it
|
|
25189
|
+
if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
|
|
25190
|
+
logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
|
|
25191
|
+
return;
|
|
25192
|
+
}
|
|
25193
|
+
|
|
25194
|
+
// For simple events (no property filters), just check event name
|
|
25195
|
+
if (!targeting) {
|
|
25196
|
+
matchResult = {
|
|
25197
|
+
matches: eventName === pendingEvent['event_name'],
|
|
25198
|
+
error: null
|
|
25199
|
+
};
|
|
25200
|
+
} else {
|
|
25201
|
+
var criteria = {
|
|
25202
|
+
'event_name': pendingEvent['event_name'],
|
|
25203
|
+
'property_filters': pendingEvent['property_filters']
|
|
25204
|
+
};
|
|
25205
|
+
matchResult = targeting['eventMatchesCriteria'](
|
|
25206
|
+
eventName,
|
|
25207
|
+
properties,
|
|
25208
|
+
criteria
|
|
25209
|
+
);
|
|
25210
|
+
}
|
|
25211
|
+
|
|
25212
|
+
if (matchResult.error) {
|
|
25213
|
+
logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
|
|
25214
|
+
return;
|
|
25215
|
+
}
|
|
25216
|
+
|
|
25217
|
+
if (!matchResult.matches) {
|
|
25218
|
+
return;
|
|
25219
|
+
}
|
|
25220
|
+
|
|
25221
|
+
logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
|
|
25222
|
+
|
|
25223
|
+
var newVariant = {
|
|
25224
|
+
'key': pendingEvent['pending_variant']['variant_key'],
|
|
25225
|
+
'value': pendingEvent['pending_variant']['variant_value'],
|
|
25226
|
+
'experiment_id': pendingEvent['pending_variant']['experiment_id'],
|
|
25227
|
+
'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
|
|
25228
|
+
};
|
|
25229
|
+
|
|
25230
|
+
this.flags.set(flagKey, newVariant);
|
|
25231
|
+
this.activatedFirstTimeEvents[eventKey] = true;
|
|
25232
|
+
|
|
25233
|
+
this.recordFirstTimeEvent(
|
|
25234
|
+
pendingEvent['flag_id'],
|
|
25235
|
+
pendingEvent['project_id'],
|
|
25236
|
+
pendingEvent['first_time_event_hash']
|
|
25237
|
+
);
|
|
25238
|
+
}, this);
|
|
25239
|
+
};
|
|
25240
|
+
|
|
25241
|
+
FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
|
|
25242
|
+
// Construct URL: {api_host}/flags/{flagId}/first-time-events
|
|
25243
|
+
return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
|
|
25244
|
+
};
|
|
25245
|
+
|
|
25246
|
+
FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
|
|
25247
|
+
var distinctId = this.getMpProperty('distinct_id');
|
|
25248
|
+
var traceparent = generateTraceparent();
|
|
25249
|
+
|
|
25250
|
+
// Build URL with query string parameters
|
|
25251
|
+
var searchParams = new URLSearchParams();
|
|
25252
|
+
searchParams.set('mp_lib', 'web');
|
|
25253
|
+
searchParams.set('$lib_version', Config.LIB_VERSION);
|
|
25254
|
+
var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
|
|
25255
|
+
|
|
25256
|
+
var payload = {
|
|
25257
|
+
'distinct_id': distinctId,
|
|
25258
|
+
'project_id': projectId,
|
|
25259
|
+
'first_time_event_hash': firstTimeEventHash
|
|
25260
|
+
};
|
|
25261
|
+
|
|
25262
|
+
logger.log('Recording first-time event for flag: ' + flagId);
|
|
25263
|
+
|
|
25264
|
+
// Fire-and-forget POST request
|
|
25265
|
+
this.fetch.call(win, url, {
|
|
25266
|
+
'method': 'POST',
|
|
25267
|
+
'headers': {
|
|
25268
|
+
'Content-Type': 'application/json',
|
|
25269
|
+
'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
|
|
25270
|
+
'traceparent': traceparent
|
|
25271
|
+
},
|
|
25272
|
+
'body': JSON.stringify(payload)
|
|
25273
|
+
}).catch(function(error) {
|
|
25274
|
+
// Silent failure - cohort sync will catch up
|
|
25275
|
+
logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
|
|
25276
|
+
});
|
|
25277
|
+
};
|
|
25278
|
+
|
|
24881
25279
|
FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
|
|
24882
25280
|
if (!this.fetchPromise) {
|
|
24883
25281
|
return new Promise(function(resolve) {
|
|
@@ -24996,6 +25394,9 @@
|
|
|
24996
25394
|
// Deprecated method
|
|
24997
25395
|
FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
|
|
24998
25396
|
|
|
25397
|
+
// Exports intended only for testing
|
|
25398
|
+
FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
|
|
25399
|
+
|
|
24999
25400
|
/* eslint camelcase: "off" */
|
|
25000
25401
|
|
|
25001
25402
|
|
|
@@ -26465,6 +26866,7 @@
|
|
|
26465
26866
|
'record_min_ms': 0,
|
|
26466
26867
|
'record_sessions_percent': 0,
|
|
26467
26868
|
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
|
|
26869
|
+
'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
|
|
26468
26870
|
'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
|
|
26469
26871
|
};
|
|
26470
26872
|
|
|
@@ -26694,7 +27096,9 @@
|
|
|
26694
27096
|
getConfigFunc: _.bind(this.get_config, this),
|
|
26695
27097
|
setConfigFunc: _.bind(this.set_config, this),
|
|
26696
27098
|
getPropertyFunc: _.bind(this.get_property, this),
|
|
26697
|
-
trackingFunc: _.bind(this.track, this)
|
|
27099
|
+
trackingFunc: _.bind(this.track, this),
|
|
27100
|
+
loadExtraBundle: load_extra_bundle,
|
|
27101
|
+
targetingSrc: this.get_config('targeting_src')
|
|
26698
27102
|
});
|
|
26699
27103
|
this.flags.init();
|
|
26700
27104
|
this['flags'] = this.flags;
|
|
@@ -26790,11 +27194,11 @@
|
|
|
26790
27194
|
|
|
26791
27195
|
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
26792
27196
|
var handleLoadedRecorder = _.bind(function() {
|
|
26793
|
-
this._recorder = this._recorder || new win[
|
|
27197
|
+
this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
|
|
26794
27198
|
this._recorder['resumeRecording'](startNewIfInactive);
|
|
26795
27199
|
}, this);
|
|
26796
27200
|
|
|
26797
|
-
if (_.isUndefined(win[
|
|
27201
|
+
if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
|
|
26798
27202
|
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
26799
27203
|
} else {
|
|
26800
27204
|
handleLoadedRecorder();
|
|
@@ -27536,6 +27940,11 @@
|
|
|
27536
27940
|
send_request_options: options
|
|
27537
27941
|
}, callback);
|
|
27538
27942
|
|
|
27943
|
+
// Check for first-time event matches
|
|
27944
|
+
if (this.flags && this.flags.checkFirstTimeEvents) {
|
|
27945
|
+
this.flags.checkFirstTimeEvents(event_name, properties);
|
|
27946
|
+
}
|
|
27947
|
+
|
|
27539
27948
|
return ret;
|
|
27540
27949
|
});
|
|
27541
27950
|
|