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.
Files changed (37) hide show
  1. package/.github/workflows/unit-tests.yml +1 -1
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/dist/mixpanel-core.cjs.js +318 -20
  5. package/dist/mixpanel-recorder.js +127 -15
  6. package/dist/mixpanel-recorder.min.js +1 -1
  7. package/dist/mixpanel-recorder.min.js.map +1 -1
  8. package/dist/mixpanel-targeting.js +2576 -0
  9. package/dist/mixpanel-targeting.min.js +2 -0
  10. package/dist/mixpanel-targeting.min.js.map +1 -0
  11. package/dist/mixpanel-with-async-modules.cjs.d.ts +522 -0
  12. package/dist/mixpanel-with-async-modules.cjs.js +9700 -0
  13. package/dist/mixpanel-with-async-recorder.cjs.js +318 -20
  14. package/dist/mixpanel-with-recorder.js +435 -26
  15. package/dist/mixpanel-with-recorder.min.js +1 -1
  16. package/dist/mixpanel.amd.js +1020 -28
  17. package/dist/mixpanel.cjs.js +1020 -28
  18. package/dist/mixpanel.globals.js +318 -20
  19. package/dist/mixpanel.min.js +179 -172
  20. package/dist/mixpanel.module.js +1020 -28
  21. package/dist/mixpanel.umd.js +1020 -28
  22. package/dist/rrweb-bundled.js +119 -5
  23. package/dist/rrweb-compiled.js +116 -5
  24. package/package.json +4 -3
  25. package/rollup.config.mjs +34 -2
  26. package/src/config.js +1 -1
  27. package/src/flags/index.js +269 -8
  28. package/src/globals.js +14 -0
  29. package/src/loaders/loader-module.js +1 -0
  30. package/src/mixpanel-core.js +12 -3
  31. package/src/recorder/index.js +2 -1
  32. package/src/targeting/event-matcher.js +97 -0
  33. package/src/targeting/index.js +11 -0
  34. package/src/targeting/loader.js +36 -0
  35. package/src/utils.js +1 -8
  36. package/.claude/settings.local.json +0 -12
  37. /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
@@ -25,6 +25,16 @@ if (typeof(window) === 'undefined') {
25
25
  win = window;
26
26
  }
27
27
 
28
+ /**
29
+ * Shared global window property names used across modules
30
+ */
31
+
32
+ // Targeting library global (used by flags and targeting modules)
33
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
34
+
35
+ // Recorder library global (used by recorder and mixpanel-core)
36
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
37
+
28
38
  function _array_like_to_array(arr, len) {
29
39
  if (len == null || len > arr.length) len = arr.length;
30
40
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -639,14 +649,16 @@ var Mirror = /*#__PURE__*/ function() {
639
649
  return this.nodeMetaMap.get(n2) || null;
640
650
  };
641
651
  // removes the node from idNodeMap
642
- // doesn't remove the node from nodeMetaMap
643
- _proto.removeNodeFromMap = function removeNodeFromMap(n2) {
652
+ // if permanent is true, also removes from nodeMetaMap
653
+ _proto.removeNodeFromMap = function removeNodeFromMap(n2, permanent) {
644
654
  var _this = this;
655
+ if (permanent === void 0) permanent = false;
645
656
  var id = this.getId(n2);
646
657
  this.idNodeMap.delete(id);
658
+ if (permanent) this.nodeMetaMap.delete(n2);
647
659
  if (n2.childNodes) {
648
660
  n2.childNodes.forEach(function(childNode) {
649
- return _this.removeNodeFromMap(childNode);
661
+ return _this.removeNodeFromMap(childNode, permanent);
650
662
  });
651
663
  }
652
664
  };
@@ -10388,6 +10400,15 @@ var StyleSheetMirror = /*#__PURE__*/ function() {
10388
10400
  _proto.generateId = function generateId() {
10389
10401
  return this.id++;
10390
10402
  };
10403
+ _proto.remove = function remove(stylesheet) {
10404
+ var id = this.styleIDMap.get(stylesheet);
10405
+ if (id !== void 0) {
10406
+ this.styleIDMap.delete(stylesheet);
10407
+ this.idStyleMap.delete(id);
10408
+ return true;
10409
+ }
10410
+ return false;
10411
+ };
10391
10412
  return StyleSheetMirror;
10392
10413
  }();
10393
10414
  function getShadowHost(n2) {
@@ -10710,7 +10731,15 @@ var MutationBuffer = /*#__PURE__*/ function() {
10710
10731
  }
10711
10732
  };
10712
10733
  while(_this.mapRemoves.length){
10713
- _this.mirror.removeNodeFromMap(_this.mapRemoves.shift());
10734
+ var removedNode = _this.mapRemoves.shift();
10735
+ if (removedNode.nodeName === "IFRAME") {
10736
+ try {
10737
+ _this.iframeManager.removeIframe(removedNode);
10738
+ } catch (e2) {}
10739
+ } else {
10740
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10741
+ }
10742
+ _this.mirror.removeNodeFromMap(removedNode);
10714
10743
  }
10715
10744
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
10716
10745
  var n2 = _step.value;
@@ -11084,6 +11113,9 @@ var MutationBuffer = /*#__PURE__*/ function() {
11084
11113
  this.shadowDomManager.reset();
11085
11114
  this.canvasManager.reset();
11086
11115
  };
11116
+ _proto.getDoc = function getDoc() {
11117
+ return this.doc;
11118
+ };
11087
11119
  return MutationBuffer;
11088
11120
  }();
11089
11121
  function deepDelete(addsSet, n2) {
@@ -11184,6 +11216,14 @@ function initMutationObserver(options, rootEl) {
11184
11216
  });
11185
11217
  return observer;
11186
11218
  }
11219
+ function removeMutationBufferForDoc(doc) {
11220
+ for(var i2 = mutationBuffers.length - 1; i2 >= 0; i2--){
11221
+ var buffer = mutationBuffers[i2];
11222
+ if (buffer.getDoc() === doc) {
11223
+ mutationBuffers.splice(i2, 1);
11224
+ }
11225
+ }
11226
+ }
11187
11227
  function initMoveObserver(param) {
11188
11228
  var mousemoveCb = param.mousemoveCb, sampling = param.sampling, doc = param.doc, mirror2 = param.mirror;
11189
11229
  if (sampling.mousemove === false) {
@@ -12199,6 +12239,8 @@ var IframeManager = /*#__PURE__*/ function() {
12199
12239
  __publicField$1(this, "crossOriginIframeMirror", new CrossOriginIframeMirror(genId));
12200
12240
  __publicField$1(this, "crossOriginIframeStyleMirror");
12201
12241
  __publicField$1(this, "crossOriginIframeRootIdMap", /* @__PURE__ */ new WeakMap());
12242
+ __publicField$1(this, "iframeContentDocumentMap", /* @__PURE__ */ new WeakMap());
12243
+ __publicField$1(this, "iframeObserverCleanupMap", /* @__PURE__ */ new WeakMap());
12202
12244
  __publicField$1(this, "mirror");
12203
12245
  __publicField$1(this, "mutationCb");
12204
12246
  __publicField$1(this, "wrappedEmit");
@@ -12220,6 +12262,31 @@ var IframeManager = /*#__PURE__*/ function() {
12220
12262
  this.iframes.set(iframeEl, true);
12221
12263
  if (iframeEl.contentWindow) this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
12222
12264
  };
12265
+ _proto.getIframeContentDocument = function getIframeContentDocument(iframeEl) {
12266
+ return this.iframeContentDocumentMap.get(iframeEl);
12267
+ };
12268
+ _proto.setObserverCleanup = function setObserverCleanup(iframeEl, cleanup) {
12269
+ this.iframeObserverCleanupMap.set(iframeEl, cleanup);
12270
+ };
12271
+ _proto.getObserverCleanup = function getObserverCleanup(iframeEl) {
12272
+ return this.iframeObserverCleanupMap.get(iframeEl);
12273
+ };
12274
+ _proto.removeIframe = function removeIframe(iframeEl) {
12275
+ var storedDoc = this.iframeContentDocumentMap.get(iframeEl);
12276
+ if (storedDoc) {
12277
+ this.stylesheetManager.cleanupStylesheetsForRemovedNode(storedDoc);
12278
+ this.mirror.removeNodeFromMap(storedDoc, true);
12279
+ }
12280
+ this.iframes.delete(iframeEl);
12281
+ this.iframeContentDocumentMap.delete(iframeEl);
12282
+ var observerCleanup = this.iframeObserverCleanupMap.get(iframeEl);
12283
+ if (observerCleanup) {
12284
+ try {
12285
+ observerCleanup();
12286
+ } catch (e2) {}
12287
+ this.iframeObserverCleanupMap.delete(iframeEl);
12288
+ }
12289
+ };
12223
12290
  _proto.addLoadListener = function addLoadListener(cb) {
12224
12291
  this.loadListener = cb;
12225
12292
  };
@@ -12238,6 +12305,9 @@ var IframeManager = /*#__PURE__*/ function() {
12238
12305
  attributes: [],
12239
12306
  isAttachIframe: true
12240
12307
  });
12308
+ if (iframeEl.contentDocument) {
12309
+ this.iframeContentDocumentMap.set(iframeEl, iframeEl.contentDocument);
12310
+ }
12241
12311
  if (this.recordCrossOriginIframes) (_a2 = iframeEl.contentWindow) == null ? void 0 : _a2.addEventListener("message", this.handleMessage.bind(this));
12242
12312
  (_b = this.loadListener) == null ? void 0 : _b.call(this, iframeEl);
12243
12313
  if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets && iframeEl.contentDocument.adoptedStyleSheets.length > 0) this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
@@ -13150,6 +13220,41 @@ var StylesheetManager = /*#__PURE__*/ function() {
13150
13220
  this.styleMirror.reset();
13151
13221
  this.trackedLinkElements = /* @__PURE__ */ new WeakSet();
13152
13222
  };
13223
+ /**
13224
+ * Cleans up stylesheets associated with a removed node.
13225
+ *
13226
+ * @param removedNode - The node that was removed from the DOM.
13227
+ */ _proto.cleanupStylesheetsForRemovedNode = function cleanupStylesheetsForRemovedNode(removedNode) {
13228
+ var _this = this;
13229
+ try {
13230
+ if (removedNode.nodeType === Node.DOCUMENT_NODE) {
13231
+ var doc = removedNode;
13232
+ if (doc.adoptedStyleSheets) {
13233
+ for(var _iterator = _create_for_of_iterator_helper_loose(doc.adoptedStyleSheets), _step; !(_step = _iterator()).done;){
13234
+ var sheet = _step.value;
13235
+ this.styleMirror.remove(sheet);
13236
+ }
13237
+ }
13238
+ }
13239
+ if (removedNode.nodeName === "STYLE") {
13240
+ var styleEl = removedNode;
13241
+ if (styleEl.sheet) {
13242
+ this.styleMirror.remove(styleEl.sheet);
13243
+ }
13244
+ }
13245
+ if (removedNode.nodeName === "LINK" && removedNode.rel === "stylesheet") {
13246
+ var linkEl = removedNode;
13247
+ if (linkEl.sheet) {
13248
+ this.styleMirror.remove(linkEl.sheet);
13249
+ }
13250
+ }
13251
+ if (removedNode.childNodes) {
13252
+ removedNode.childNodes.forEach(function(child) {
13253
+ _this.cleanupStylesheetsForRemovedNode(child);
13254
+ });
13255
+ }
13256
+ } catch (e2) {}
13257
+ };
13153
13258
  // TODO: take snapshot on stylesheet reload by applying event listener
13154
13259
  _proto.trackStylesheetInLinkElement = function trackStylesheetInLinkElement(_linkEl) {};
13155
13260
  return StylesheetManager;
@@ -13604,7 +13709,23 @@ function record(options) {
13604
13709
  };
13605
13710
  iframeManager.addLoadListener(function(iframeEl) {
13606
13711
  try {
13607
- handlers.push(observe(iframeEl.contentDocument));
13712
+ var iframeDoc = iframeEl.contentDocument;
13713
+ var iframeHandler = observe(iframeDoc);
13714
+ handlers.push(iframeHandler);
13715
+ var existingCleanup = iframeManager.getObserverCleanup(iframeEl);
13716
+ iframeManager.setObserverCleanup(iframeEl, function() {
13717
+ if (existingCleanup) {
13718
+ try {
13719
+ existingCleanup();
13720
+ } catch (e2) {}
13721
+ }
13722
+ try {
13723
+ iframeHandler();
13724
+ var idx = handlers.indexOf(iframeHandler);
13725
+ if (idx !== -1) handlers.splice(idx, 1);
13726
+ removeMutationBufferForDoc(iframeDoc);
13727
+ } catch (e2) {}
13728
+ });
13608
13729
  } catch (error) {
13609
13730
  console.warn(error);
13610
13731
  }
@@ -13861,7 +13982,7 @@ function classMatchesRegex(node2, regex, checkAncestors) {
13861
13982
  }
13862
13983
  return classMatchesRegex(index.parentNode(node2), regex);
13863
13984
  }
13864
- function getDefaultExportFromCjs(x2) {
13985
+ function getDefaultExportFromCjs$3(x2) {
13865
13986
  return x2 && x2.__esModule && Object.prototype.hasOwnProperty.call(x2, "default") ? x2["default"] : x2;
13866
13987
  }
13867
13988
  function getAugmentedNamespace(n) {
@@ -17976,7 +18097,7 @@ postcss.Node = Node2;
17976
18097
  LazyResult2.registerPostcss(postcss);
17977
18098
  var postcss_1 = postcss;
17978
18099
  postcss.default = postcss;
17979
- var postcss$1 = /* @__PURE__ */ getDefaultExportFromCjs(postcss_1);
18100
+ var postcss$1 = /* @__PURE__ */ getDefaultExportFromCjs$3(postcss_1);
17980
18101
  postcss$1.stringify;
17981
18102
  postcss$1.fromJSON;
17982
18103
  postcss$1.plugin;
@@ -18853,7 +18974,7 @@ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]'
18853
18974
 
18854
18975
  var Config = {
18855
18976
  DEBUG: false,
18856
- LIB_VERSION: '2.74.0'
18977
+ LIB_VERSION: '2.75.0'
18857
18978
  };
18858
18979
 
18859
18980
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -19059,15 +19180,8 @@ _.isArray = nativeIsArray || function(obj) {
19059
19180
  return toString.call(obj) === '[object Array]';
19060
19181
  };
19061
19182
 
19062
- // from a comment on http://dbj.org/dbj/?p=286
19063
- // fails on only one very rare and deliberate custom object:
19064
- // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
19065
19183
  _.isFunction = function(f) {
19066
- try {
19067
- return /^\s*\bfunction\b/.test(f);
19068
- } catch (x) {
19069
- return false;
19070
- }
19184
+ return typeof f === 'function';
19071
19185
  };
19072
19186
 
19073
19187
  _.isArguments = function(obj) {
@@ -23761,7 +23875,590 @@ Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
23761
23875
  }
23762
23876
  });
23763
23877
 
23764
- win['__mp_recorder'] = MixpanelRecorder;
23878
+ win[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
23879
+
23880
+ function getDefaultExportFromCjs (x) {
23881
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
23882
+ }
23883
+
23884
+ var logic$1 = {exports: {}};
23885
+
23886
+ /* globals define,module */
23887
+ var logic = logic$1.exports;
23888
+
23889
+ var hasRequiredLogic;
23890
+
23891
+ function requireLogic () {
23892
+ if (hasRequiredLogic) return logic$1.exports;
23893
+ hasRequiredLogic = 1;
23894
+ (function (module, exports) {
23895
+ (function(root, factory) {
23896
+ {
23897
+ module.exports = factory();
23898
+ }
23899
+ }(logic, function() {
23900
+ /* globals console:false */
23901
+
23902
+ if ( ! Array.isArray) {
23903
+ Array.isArray = function(arg) {
23904
+ return Object.prototype.toString.call(arg) === "[object Array]";
23905
+ };
23906
+ }
23907
+
23908
+ /**
23909
+ * Return an array that contains no duplicates (original not modified)
23910
+ * @param {array} array Original reference array
23911
+ * @return {array} New array with no duplicates
23912
+ */
23913
+ function arrayUnique(array) {
23914
+ var a = [];
23915
+ for (var i=0, l=array.length; i<l; i++) {
23916
+ if (a.indexOf(array[i]) === -1) {
23917
+ a.push(array[i]);
23918
+ }
23919
+ }
23920
+ return a;
23921
+ }
23922
+
23923
+ var jsonLogic = {};
23924
+ var operations = {
23925
+ "==": function(a, b) {
23926
+ return a == b;
23927
+ },
23928
+ "===": function(a, b) {
23929
+ return a === b;
23930
+ },
23931
+ "!=": function(a, b) {
23932
+ return a != b;
23933
+ },
23934
+ "!==": function(a, b) {
23935
+ return a !== b;
23936
+ },
23937
+ ">": function(a, b) {
23938
+ return a > b;
23939
+ },
23940
+ ">=": function(a, b) {
23941
+ return a >= b;
23942
+ },
23943
+ "<": function(a, b, c) {
23944
+ return (c === undefined) ? a < b : (a < b) && (b < c);
23945
+ },
23946
+ "<=": function(a, b, c) {
23947
+ return (c === undefined) ? a <= b : (a <= b) && (b <= c);
23948
+ },
23949
+ "!!": function(a) {
23950
+ return jsonLogic.truthy(a);
23951
+ },
23952
+ "!": function(a) {
23953
+ return !jsonLogic.truthy(a);
23954
+ },
23955
+ "%": function(a, b) {
23956
+ return a % b;
23957
+ },
23958
+ "log": function(a) {
23959
+ console.log(a); return a;
23960
+ },
23961
+ "in": function(a, b) {
23962
+ if (!b || typeof b.indexOf === "undefined") return false;
23963
+ return (b.indexOf(a) !== -1);
23964
+ },
23965
+ "cat": function() {
23966
+ return Array.prototype.join.call(arguments, "");
23967
+ },
23968
+ "substr": function(source, start, end) {
23969
+ if (end < 0) {
23970
+ // JavaScript doesn't support negative end, this emulates PHP behavior
23971
+ var temp = String(source).substr(start);
23972
+ return temp.substr(0, temp.length + end);
23973
+ }
23974
+ return String(source).substr(start, end);
23975
+ },
23976
+ "+": function() {
23977
+ return Array.prototype.reduce.call(arguments, function(a, b) {
23978
+ return parseFloat(a, 10) + parseFloat(b, 10);
23979
+ }, 0);
23980
+ },
23981
+ "*": function() {
23982
+ return Array.prototype.reduce.call(arguments, function(a, b) {
23983
+ return parseFloat(a, 10) * parseFloat(b, 10);
23984
+ });
23985
+ },
23986
+ "-": function(a, b) {
23987
+ if (b === undefined) {
23988
+ return -a;
23989
+ } else {
23990
+ return a - b;
23991
+ }
23992
+ },
23993
+ "/": function(a, b) {
23994
+ return a / b;
23995
+ },
23996
+ "min": function() {
23997
+ return Math.min.apply(this, arguments);
23998
+ },
23999
+ "max": function() {
24000
+ return Math.max.apply(this, arguments);
24001
+ },
24002
+ "merge": function() {
24003
+ return Array.prototype.reduce.call(arguments, function(a, b) {
24004
+ return a.concat(b);
24005
+ }, []);
24006
+ },
24007
+ "var": function(a, b) {
24008
+ var not_found = (b === undefined) ? null : b;
24009
+ var data = this;
24010
+ if (typeof a === "undefined" || a==="" || a===null) {
24011
+ return data;
24012
+ }
24013
+ var sub_props = String(a).split(".");
24014
+ for (var i = 0; i < sub_props.length; i++) {
24015
+ if (data === null || data === undefined) {
24016
+ return not_found;
24017
+ }
24018
+ // Descending into data
24019
+ data = data[sub_props[i]];
24020
+ if (data === undefined) {
24021
+ return not_found;
24022
+ }
24023
+ }
24024
+ return data;
24025
+ },
24026
+ "missing": function() {
24027
+ /*
24028
+ Missing can receive many keys as many arguments, like {"missing:[1,2]}
24029
+ Missing can also receive *one* argument that is an array of keys,
24030
+ which typically happens if it's actually acting on the output of another command
24031
+ (like 'if' or 'merge')
24032
+ */
24033
+
24034
+ var missing = [];
24035
+ var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;
24036
+
24037
+ for (var i = 0; i < keys.length; i++) {
24038
+ var key = keys[i];
24039
+ var value = jsonLogic.apply({"var": key}, this);
24040
+ if (value === null || value === "") {
24041
+ missing.push(key);
24042
+ }
24043
+ }
24044
+
24045
+ return missing;
24046
+ },
24047
+ "missing_some": function(need_count, options) {
24048
+ // missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
24049
+ var are_missing = jsonLogic.apply({"missing": options}, this);
24050
+
24051
+ if (options.length - are_missing.length >= need_count) {
24052
+ return [];
24053
+ } else {
24054
+ return are_missing;
24055
+ }
24056
+ },
24057
+ };
24058
+
24059
+ jsonLogic.is_logic = function(logic) {
24060
+ return (
24061
+ typeof logic === "object" && // An object
24062
+ logic !== null && // but not null
24063
+ ! Array.isArray(logic) && // and not an array
24064
+ Object.keys(logic).length === 1 // with exactly one key
24065
+ );
24066
+ };
24067
+
24068
+ /*
24069
+ This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
24070
+
24071
+ Spec and rationale here: http://jsonlogic.com/truthy
24072
+ */
24073
+ jsonLogic.truthy = function(value) {
24074
+ if (Array.isArray(value) && value.length === 0) {
24075
+ return false;
24076
+ }
24077
+ return !! value;
24078
+ };
24079
+
24080
+
24081
+ jsonLogic.get_operator = function(logic) {
24082
+ return Object.keys(logic)[0];
24083
+ };
24084
+
24085
+ jsonLogic.get_values = function(logic) {
24086
+ return logic[jsonLogic.get_operator(logic)];
24087
+ };
24088
+
24089
+ jsonLogic.apply = function(logic, data) {
24090
+ // Does this array contain logic? Only one way to find out.
24091
+ if (Array.isArray(logic)) {
24092
+ return logic.map(function(l) {
24093
+ return jsonLogic.apply(l, data);
24094
+ });
24095
+ }
24096
+ // You've recursed to a primitive, stop!
24097
+ if ( ! jsonLogic.is_logic(logic) ) {
24098
+ return logic;
24099
+ }
24100
+
24101
+ var op = jsonLogic.get_operator(logic);
24102
+ var values = logic[op];
24103
+ var i;
24104
+ var current;
24105
+ var scopedLogic;
24106
+ var scopedData;
24107
+ var initial;
24108
+
24109
+ // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
24110
+ if ( ! Array.isArray(values)) {
24111
+ values = [values];
24112
+ }
24113
+
24114
+ // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
24115
+ if (op === "if" || op == "?:") {
24116
+ /* 'if' should be called with a odd number of parameters, 3 or greater
24117
+ This works on the pattern:
24118
+ if( 0 ){ 1 }else{ 2 };
24119
+ if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
24120
+ if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
24121
+
24122
+ The implementation is:
24123
+ For pairs of values (0,1 then 2,3 then 4,5 etc)
24124
+ If the first evaluates truthy, evaluate and return the second
24125
+ If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
24126
+ given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
24127
+ given 0 parameters, return NULL (not great practice, but there was no Else)
24128
+ */
24129
+ for (i = 0; i < values.length - 1; i += 2) {
24130
+ if ( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) {
24131
+ return jsonLogic.apply(values[i+1], data);
24132
+ }
24133
+ }
24134
+ if (values.length === i+1) {
24135
+ return jsonLogic.apply(values[i], data);
24136
+ }
24137
+ return null;
24138
+ } else if (op === "and") { // Return first falsy, or last
24139
+ for (i=0; i < values.length; i+=1) {
24140
+ current = jsonLogic.apply(values[i], data);
24141
+ if ( ! jsonLogic.truthy(current)) {
24142
+ return current;
24143
+ }
24144
+ }
24145
+ return current; // Last
24146
+ } else if (op === "or") {// Return first truthy, or last
24147
+ for (i=0; i < values.length; i+=1) {
24148
+ current = jsonLogic.apply(values[i], data);
24149
+ if ( jsonLogic.truthy(current) ) {
24150
+ return current;
24151
+ }
24152
+ }
24153
+ return current; // Last
24154
+ } else if (op === "filter") {
24155
+ scopedData = jsonLogic.apply(values[0], data);
24156
+ scopedLogic = values[1];
24157
+
24158
+ if ( ! Array.isArray(scopedData)) {
24159
+ return [];
24160
+ }
24161
+ // Return only the elements from the array in the first argument,
24162
+ // that return truthy when passed to the logic in the second argument.
24163
+ // For parity with JavaScript, reindex the returned array
24164
+ return scopedData.filter(function(datum) {
24165
+ return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum));
24166
+ });
24167
+ } else if (op === "map") {
24168
+ scopedData = jsonLogic.apply(values[0], data);
24169
+ scopedLogic = values[1];
24170
+
24171
+ if ( ! Array.isArray(scopedData)) {
24172
+ return [];
24173
+ }
24174
+
24175
+ return scopedData.map(function(datum) {
24176
+ return jsonLogic.apply(scopedLogic, datum);
24177
+ });
24178
+ } else if (op === "reduce") {
24179
+ scopedData = jsonLogic.apply(values[0], data);
24180
+ scopedLogic = values[1];
24181
+ initial = typeof values[2] !== "undefined" ? jsonLogic.apply(values[2], data) : null;
24182
+
24183
+ if ( ! Array.isArray(scopedData)) {
24184
+ return initial;
24185
+ }
24186
+
24187
+ return scopedData.reduce(
24188
+ function(accumulator, current) {
24189
+ return jsonLogic.apply(
24190
+ scopedLogic,
24191
+ {current: current, accumulator: accumulator}
24192
+ );
24193
+ },
24194
+ initial
24195
+ );
24196
+ } else if (op === "all") {
24197
+ scopedData = jsonLogic.apply(values[0], data);
24198
+ scopedLogic = values[1];
24199
+ // All of an empty set is false. Note, some and none have correct fallback after the for loop
24200
+ if ( ! Array.isArray(scopedData) || ! scopedData.length) {
24201
+ return false;
24202
+ }
24203
+ for (i=0; i < scopedData.length; i+=1) {
24204
+ if ( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
24205
+ return false; // First falsy, short circuit
24206
+ }
24207
+ }
24208
+ return true; // All were truthy
24209
+ } else if (op === "none") {
24210
+ scopedData = jsonLogic.apply(values[0], data);
24211
+ scopedLogic = values[1];
24212
+
24213
+ if ( ! Array.isArray(scopedData) || ! scopedData.length) {
24214
+ return true;
24215
+ }
24216
+ for (i=0; i < scopedData.length; i+=1) {
24217
+ if ( jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
24218
+ return false; // First truthy, short circuit
24219
+ }
24220
+ }
24221
+ return true; // None were truthy
24222
+ } else if (op === "some") {
24223
+ scopedData = jsonLogic.apply(values[0], data);
24224
+ scopedLogic = values[1];
24225
+
24226
+ if ( ! Array.isArray(scopedData) || ! scopedData.length) {
24227
+ return false;
24228
+ }
24229
+ for (i=0; i < scopedData.length; i+=1) {
24230
+ if ( jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
24231
+ return true; // First truthy, short circuit
24232
+ }
24233
+ }
24234
+ return false; // None were truthy
24235
+ }
24236
+
24237
+ // Everyone else gets immediate depth-first recursion
24238
+ values = values.map(function(val) {
24239
+ return jsonLogic.apply(val, data);
24240
+ });
24241
+
24242
+
24243
+ // The operation is called with "data" bound to its "this" and "values" passed as arguments.
24244
+ // Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
24245
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
24246
+ if (operations.hasOwnProperty(op) && typeof operations[op] === "function") {
24247
+ return operations[op].apply(data, values);
24248
+ } else if (op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position
24249
+ var sub_ops = String(op).split(".");
24250
+ var operation = operations;
24251
+ for (i = 0; i < sub_ops.length; i++) {
24252
+ if (!operation.hasOwnProperty(sub_ops[i])) {
24253
+ throw new Error("Unrecognized operation " + op +
24254
+ " (failed at " + sub_ops.slice(0, i+1).join(".") + ")");
24255
+ }
24256
+ // Descending into operations
24257
+ operation = operation[sub_ops[i]];
24258
+ }
24259
+
24260
+ return operation.apply(data, values);
24261
+ }
24262
+
24263
+ throw new Error("Unrecognized operation " + op );
24264
+ };
24265
+
24266
+ jsonLogic.uses_data = function(logic) {
24267
+ var collection = [];
24268
+
24269
+ if (jsonLogic.is_logic(logic)) {
24270
+ var op = jsonLogic.get_operator(logic);
24271
+ var values = logic[op];
24272
+
24273
+ if ( ! Array.isArray(values)) {
24274
+ values = [values];
24275
+ }
24276
+
24277
+ if (op === "var") {
24278
+ // This doesn't cover the case where the arg to var is itself a rule.
24279
+ collection.push(values[0]);
24280
+ } else {
24281
+ // Recursion!
24282
+ values.forEach(function(val) {
24283
+ collection.push.apply(collection, jsonLogic.uses_data(val) );
24284
+ });
24285
+ }
24286
+ }
24287
+
24288
+ return arrayUnique(collection);
24289
+ };
24290
+
24291
+ jsonLogic.add_operation = function(name, code) {
24292
+ operations[name] = code;
24293
+ };
24294
+
24295
+ jsonLogic.rm_operation = function(name) {
24296
+ delete operations[name];
24297
+ };
24298
+
24299
+ jsonLogic.rule_like = function(rule, pattern) {
24300
+ // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?");
24301
+ if (pattern === rule) {
24302
+ return true;
24303
+ } // TODO : Deep object equivalency?
24304
+ if (pattern === "@") {
24305
+ return true;
24306
+ } // Wildcard!
24307
+ if (pattern === "number") {
24308
+ return (typeof rule === "number");
24309
+ }
24310
+ if (pattern === "string") {
24311
+ return (typeof rule === "string");
24312
+ }
24313
+ if (pattern === "array") {
24314
+ // !logic test might be superfluous in JavaScript
24315
+ return Array.isArray(rule) && ! jsonLogic.is_logic(rule);
24316
+ }
24317
+
24318
+ if (jsonLogic.is_logic(pattern)) {
24319
+ if (jsonLogic.is_logic(rule)) {
24320
+ var pattern_op = jsonLogic.get_operator(pattern);
24321
+ var rule_op = jsonLogic.get_operator(rule);
24322
+
24323
+ if (pattern_op === "@" || pattern_op === rule_op) {
24324
+ // echo "\nOperators match, go deeper\n";
24325
+ return jsonLogic.rule_like(
24326
+ jsonLogic.get_values(rule, false),
24327
+ jsonLogic.get_values(pattern, false)
24328
+ );
24329
+ }
24330
+ }
24331
+ return false; // pattern is logic, rule isn't, can't be eq
24332
+ }
24333
+
24334
+ if (Array.isArray(pattern)) {
24335
+ if (Array.isArray(rule)) {
24336
+ if (pattern.length !== rule.length) {
24337
+ return false;
24338
+ }
24339
+ /*
24340
+ Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
24341
+ */
24342
+ for (var i = 0; i < pattern.length; i += 1) {
24343
+ // If any fail, we fail
24344
+ if ( ! jsonLogic.rule_like(rule[i], pattern[i])) {
24345
+ return false;
24346
+ }
24347
+ }
24348
+ return true; // If they *all* passed, we pass
24349
+ } else {
24350
+ return false; // Pattern is array, rule isn't
24351
+ }
24352
+ }
24353
+
24354
+ // Not logic, not array, not a === match for rule.
24355
+ return false;
24356
+ };
24357
+
24358
+ return jsonLogic;
24359
+ }));
24360
+ } (logic$1));
24361
+ return logic$1.exports;
24362
+ }
24363
+
24364
+ var logicExports = requireLogic();
24365
+ var jsonLogic = /*@__PURE__*/getDefaultExportFromCjs(logicExports);
24366
+
24367
+ /**
24368
+ * Shared helper to recursively lowercase strings in nested structures
24369
+ * @param {*} obj - Value to process
24370
+ * @param {boolean} lowercaseKeys - Whether to lowercase object keys
24371
+ * @returns {*} Processed value with lowercased strings
24372
+ */
24373
+ var lowercaseJson = function(obj, lowercaseKeys) {
24374
+ if (obj === null || obj === undefined) {
24375
+ return obj;
24376
+ } else if (typeof obj === 'string') {
24377
+ return obj.toLowerCase();
24378
+ } else if (Array.isArray(obj)) {
24379
+ return obj.map(function(item) {
24380
+ return lowercaseJson(item, lowercaseKeys);
24381
+ });
24382
+ } else if (obj === Object(obj)) {
24383
+ var result = {};
24384
+ for (var key in obj) {
24385
+ if (obj.hasOwnProperty(key)) {
24386
+ var newKey = lowercaseKeys && typeof key === 'string' ? key.toLowerCase() : key;
24387
+ result[newKey] = lowercaseJson(obj[key], lowercaseKeys);
24388
+ }
24389
+ }
24390
+ return result;
24391
+ } else {
24392
+ return obj;
24393
+ }
24394
+ };
24395
+
24396
+ /**
24397
+ * Lowercase all string keys and values in a nested structure
24398
+ * @param {*} val - Value to process
24399
+ * @returns {*} Processed value with lowercased strings
24400
+ */
24401
+ var lowercaseKeysAndValues = function(val) {
24402
+ return lowercaseJson(val, true);
24403
+ };
24404
+
24405
+ /**
24406
+ * Lowercase only leaf node string values in a nested structure (keys unchanged)
24407
+ * @param {*} val - Value to process
24408
+ * @returns {*} Processed value with lowercased leaf strings
24409
+ */
24410
+ var lowercaseOnlyLeafNodes = function(val) {
24411
+ return lowercaseJson(val, false);
24412
+ };
24413
+
24414
+ /**
24415
+ * Check if an event matches the given criteria
24416
+ * @param {string} eventName - The name of the event being checked
24417
+ * @param {Object} properties - Event properties to evaluate against property filters
24418
+ * @param {Object} criteria - Criteria to match against, with:
24419
+ * - event_name: string - Required event name (case-sensitive match)
24420
+ * - property_filters: Object - Optional JsonLogic filters for properties
24421
+ * @returns {Object} Result object with:
24422
+ * - matches: boolean - Whether the event matches the criteria
24423
+ * - error: string|undefined - Error message if evaluation failed
24424
+ */
24425
+ var eventMatchesCriteria = function(eventName, properties, criteria) {
24426
+ // Check exact event name match (case-sensitive)
24427
+ if (eventName !== criteria.event_name) {
24428
+ return { matches: false };
24429
+ }
24430
+
24431
+ // Evaluate property filters using JsonLogic
24432
+ var propertyFilters = criteria.property_filters;
24433
+ var filtersMatch = true; // default to true if no filters
24434
+
24435
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
24436
+ try {
24437
+ // Lowercase all keys and values in event properties for case-insensitive matching
24438
+ var lowercasedProperties = lowercaseKeysAndValues(properties || {});
24439
+
24440
+ // Lowercase only leaf nodes in JsonLogic filters (keep operators intact)
24441
+ var lowercasedFilters = lowercaseOnlyLeafNodes(propertyFilters);
24442
+
24443
+ filtersMatch = jsonLogic.apply(lowercasedFilters, lowercasedProperties);
24444
+ } catch (error) {
24445
+ return {
24446
+ matches: false,
24447
+ error: error.toString()
24448
+ };
24449
+ }
24450
+ }
24451
+
24452
+ return { matches: filtersMatch };
24453
+ };
24454
+
24455
+ // Create targeting library object
24456
+ var targetingLibrary = {};
24457
+ targetingLibrary['eventMatchesCriteria'] = eventMatchesCriteria;
24458
+
24459
+ // Set global Promise (use bracket notation to prevent minification)
24460
+ // This is the ONE AND ONLY global - matches recorder pattern
24461
+ win[TARGETING_GLOBAL_NAME] = Promise.resolve(targetingLibrary);
23765
24462
 
23766
24463
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
23767
24464
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
@@ -24732,14 +25429,62 @@ Autocapture.prototype.stopDeadClickTracking = function() {
24732
25429
  // TODO integrate error_reporter from mixpanel instance
24733
25430
  safewrapClass(Autocapture);
24734
25431
 
24735
- var logger = console_with_prefix('flags');
25432
+ /**
25433
+ * Get the promise-based targeting loader
25434
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
25435
+ * @param {string} targetingSrc - URL to targeting bundle
25436
+ * @returns {Promise} Promise that resolves with targeting library
25437
+ */
25438
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
25439
+ // Return existing promise if already initialized or loading
25440
+ if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
25441
+ return win[TARGETING_GLOBAL_NAME];
25442
+ }
25443
+
25444
+ // Create loading promise and set it as the global immediately
25445
+ // This makes minified build behavior consistent with dev/CJS builds
25446
+ win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
25447
+ loadExtraBundle(targetingSrc, resolve);
25448
+ }).then(function () {
25449
+ var p = win[TARGETING_GLOBAL_NAME];
25450
+ if (p && typeof p.then === 'function') {
25451
+ return p;
25452
+ }
25453
+ throw new Error('targeting failed to load');
25454
+ }).catch(function (err) {
25455
+ delete win[TARGETING_GLOBAL_NAME];
25456
+ throw err;
25457
+ });
24736
25458
 
25459
+ return win[TARGETING_GLOBAL_NAME];
25460
+ };
25461
+
25462
+ var logger = console_with_prefix('flags');
24737
25463
  var FLAGS_CONFIG_KEY = 'flags';
24738
25464
 
24739
25465
  var CONFIG_CONTEXT = 'context';
24740
25466
  var CONFIG_DEFAULTS = {};
24741
25467
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
24742
25468
 
25469
+ /**
25470
+ * Generate a unique key for a pending first-time event
25471
+ * @param {string} flagKey - The flag key
25472
+ * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
25473
+ * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
25474
+ */
25475
+ var getPendingEventKey = function(flagKey, firstTimeEventHash) {
25476
+ return flagKey + ':' + firstTimeEventHash;
25477
+ };
25478
+
25479
+ /**
25480
+ * Extract the flag key from a pending event key
25481
+ * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
25482
+ * @returns {string} The flag key portion
25483
+ */
25484
+ var getFlagKeyFromPendingEventKey = function(eventKey) {
25485
+ return eventKey.split(':')[0];
25486
+ };
25487
+
24743
25488
  /**
24744
25489
  * FeatureFlagManager: support for Mixpanel's feature flagging product
24745
25490
  * @constructor
@@ -24751,6 +25496,8 @@ var FeatureFlagManager = function(initOptions) {
24751
25496
  this.setMpConfig = initOptions.setConfigFunc;
24752
25497
  this.getMpProperty = initOptions.getPropertyFunc;
24753
25498
  this.track = initOptions.trackingFunc;
25499
+ this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
25500
+ this.targetingSrc = initOptions.targetingSrc || '';
24754
25501
  };
24755
25502
 
24756
25503
  FeatureFlagManager.prototype.init = function() {
@@ -24763,6 +25510,8 @@ FeatureFlagManager.prototype.init = function() {
24763
25510
  this.fetchFlags();
24764
25511
 
24765
25512
  this.trackedFeatures = new Set();
25513
+ this.pendingFirstTimeEvents = {};
25514
+ this.activatedFirstTimeEvents = {};
24766
25515
  };
24767
25516
 
24768
25517
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -24843,17 +25592,78 @@ FeatureFlagManager.prototype.fetchFlags = function() {
24843
25592
  throw new Error('No flags in API response');
24844
25593
  }
24845
25594
  var flags = new Map();
25595
+ var pendingFirstTimeEvents = {};
25596
+
25597
+ // Process flags from response
24846
25598
  _.each(responseFlags, function(data, key) {
24847
- flags.set(key, {
24848
- 'key': data['variant_key'],
24849
- 'value': data['variant_value'],
24850
- 'experiment_id': data['experiment_id'],
24851
- 'is_experiment_active': data['is_experiment_active'],
24852
- 'is_qa_tester': data['is_qa_tester']
25599
+ // Check if this flag has any activated first-time events this session
25600
+ var hasActivatedEvent = false;
25601
+ var prefix = key + ':';
25602
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
25603
+ if (eventKey.startsWith(prefix)) {
25604
+ hasActivatedEvent = true;
25605
+ }
24853
25606
  });
24854
- });
25607
+
25608
+ if (hasActivatedEvent) {
25609
+ // Preserve the activated variant, don't overwrite with server's current variant
25610
+ var currentFlag = this.flags && this.flags.get(key);
25611
+ if (currentFlag) {
25612
+ flags.set(key, currentFlag);
25613
+ }
25614
+ } else {
25615
+ // Use server's current variant
25616
+ flags.set(key, {
25617
+ 'key': data['variant_key'],
25618
+ 'value': data['variant_value'],
25619
+ 'experiment_id': data['experiment_id'],
25620
+ 'is_experiment_active': data['is_experiment_active'],
25621
+ 'is_qa_tester': data['is_qa_tester']
25622
+ });
25623
+ }
25624
+ }, this);
25625
+
25626
+ // Process top-level pending_first_time_events array
25627
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
25628
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
25629
+ _.each(topLevelDefinitions, function(def) {
25630
+ var flagKey = def['flag_key'];
25631
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
25632
+
25633
+ // Skip if this specific event has already been activated this session
25634
+ if (this.activatedFirstTimeEvents[eventKey]) {
25635
+ return;
25636
+ }
25637
+
25638
+ // Store pending event definition using composite key
25639
+ pendingFirstTimeEvents[eventKey] = {
25640
+ 'flag_key': flagKey,
25641
+ 'flag_id': def['flag_id'],
25642
+ 'project_id': def['project_id'],
25643
+ 'first_time_event_hash': def['first_time_event_hash'],
25644
+ 'event_name': def['event_name'],
25645
+ 'property_filters': def['property_filters'],
25646
+ 'pending_variant': def['pending_variant']
25647
+ };
25648
+ }, this);
25649
+ }
25650
+
25651
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
25652
+ if (this.activatedFirstTimeEvents) {
25653
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
25654
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
25655
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
25656
+ // Keep the activated flag even though it's not in the new response
25657
+ flags.set(flagKey, this.flags.get(flagKey));
25658
+ }
25659
+ }, this);
25660
+ }
25661
+
24855
25662
  this.flags = flags;
25663
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
24856
25664
  this._traceparent = traceparent;
25665
+
25666
+ this._loadTargetingIfNeeded();
24857
25667
  }.bind(this)).catch(function(error) {
24858
25668
  this.markFetchComplete();
24859
25669
  logger.error(error);
@@ -24877,6 +25687,177 @@ FeatureFlagManager.prototype.markFetchComplete = function() {
24877
25687
  this._fetchInProgressStartTime = null;
24878
25688
  };
24879
25689
 
25690
+ /**
25691
+ * Proactively load targeting bundle if any pending events have property filters
25692
+ */
25693
+ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
25694
+ var hasPropertyFilters = false;
25695
+ _.each(this.pendingFirstTimeEvents, function(evt) {
25696
+ if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
25697
+ hasPropertyFilters = true;
25698
+ }
25699
+ });
25700
+
25701
+ if (hasPropertyFilters) {
25702
+ this.getTargeting().then(function() {
25703
+ logger.log('targeting loaded for property filter evaluation');
25704
+ });
25705
+ }
25706
+ };
25707
+
25708
+ /**
25709
+ * Get the targeting library (initializes if not already loaded)
25710
+ * This method is primarily for testing - production code should rely on automatic loading
25711
+ * @returns {Promise} Promise that resolves with targeting library
25712
+ */
25713
+ FeatureFlagManager.prototype.getTargeting = function() {
25714
+ return getTargetingPromise(
25715
+ this.loadExtraBundle.bind(this),
25716
+ this.targetingSrc
25717
+ ).catch(function(error) {
25718
+ logger.error('Failed to load targeting: ' + error);
25719
+ }.bind(this));
25720
+ };
25721
+
25722
+ /**
25723
+ * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
25724
+ * @param {string} eventName - The name of the event being tracked
25725
+ * @param {Object} properties - Event properties to evaluate against property filters
25726
+ *
25727
+ * When a match is found (event name matches and property filters pass), this method:
25728
+ * - Switches the flag to the pending variant
25729
+ * - Marks the event as activated for this session
25730
+ * - Records the activation via the API (fire-and-forget)
25731
+ */
25732
+ FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
25733
+ if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
25734
+ return;
25735
+ }
25736
+
25737
+ // Check if targeting promise exists (either bundled or async loaded)
25738
+ if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
25739
+ win[TARGETING_GLOBAL_NAME].then(function(library) {
25740
+ this._processFirstTimeEventCheck(eventName, properties, library);
25741
+ }.bind(this)).catch(function() {
25742
+ // If targeting failed to load, process with null
25743
+ // Events without property filters will still match
25744
+ this._processFirstTimeEventCheck(eventName, properties, null);
25745
+ }.bind(this));
25746
+ } else {
25747
+ // No targeting available, process with null
25748
+ // Events without property filters will still match
25749
+ this._processFirstTimeEventCheck(eventName, properties, null);
25750
+ }
25751
+ };
25752
+
25753
+ /**
25754
+ * Internal method to process first-time event checks with loaded targeting library
25755
+ * @param {string} eventName - The name of the event being tracked
25756
+ * @param {Object} properties - Event properties to evaluate against property filters
25757
+ * @param {Object} targeting - The loaded targeting library
25758
+ */
25759
+ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
25760
+ _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
25761
+ if (this.activatedFirstTimeEvents[eventKey]) {
25762
+ return;
25763
+ }
25764
+
25765
+ var flagKey = pendingEvent['flag_key'];
25766
+
25767
+ // Use targeting module to check if event matches
25768
+ var matchResult;
25769
+
25770
+ // If no targeting library and event has property filters, skip it
25771
+ if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
25772
+ logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
25773
+ return;
25774
+ }
25775
+
25776
+ // For simple events (no property filters), just check event name
25777
+ if (!targeting) {
25778
+ matchResult = {
25779
+ matches: eventName === pendingEvent['event_name'],
25780
+ error: null
25781
+ };
25782
+ } else {
25783
+ var criteria = {
25784
+ 'event_name': pendingEvent['event_name'],
25785
+ 'property_filters': pendingEvent['property_filters']
25786
+ };
25787
+ matchResult = targeting['eventMatchesCriteria'](
25788
+ eventName,
25789
+ properties,
25790
+ criteria
25791
+ );
25792
+ }
25793
+
25794
+ if (matchResult.error) {
25795
+ logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
25796
+ return;
25797
+ }
25798
+
25799
+ if (!matchResult.matches) {
25800
+ return;
25801
+ }
25802
+
25803
+ logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
25804
+
25805
+ var newVariant = {
25806
+ 'key': pendingEvent['pending_variant']['variant_key'],
25807
+ 'value': pendingEvent['pending_variant']['variant_value'],
25808
+ 'experiment_id': pendingEvent['pending_variant']['experiment_id'],
25809
+ 'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
25810
+ };
25811
+
25812
+ this.flags.set(flagKey, newVariant);
25813
+ this.activatedFirstTimeEvents[eventKey] = true;
25814
+
25815
+ this.recordFirstTimeEvent(
25816
+ pendingEvent['flag_id'],
25817
+ pendingEvent['project_id'],
25818
+ pendingEvent['first_time_event_hash']
25819
+ );
25820
+ }, this);
25821
+ };
25822
+
25823
+ FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
25824
+ // Construct URL: {api_host}/flags/{flagId}/first-time-events
25825
+ return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
25826
+ };
25827
+
25828
+ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
25829
+ var distinctId = this.getMpProperty('distinct_id');
25830
+ var traceparent = generateTraceparent();
25831
+
25832
+ // Build URL with query string parameters
25833
+ var searchParams = new URLSearchParams();
25834
+ searchParams.set('mp_lib', 'web');
25835
+ searchParams.set('$lib_version', Config.LIB_VERSION);
25836
+ var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
25837
+
25838
+ var payload = {
25839
+ 'distinct_id': distinctId,
25840
+ 'project_id': projectId,
25841
+ 'first_time_event_hash': firstTimeEventHash
25842
+ };
25843
+
25844
+ logger.log('Recording first-time event for flag: ' + flagId);
25845
+
25846
+ // Fire-and-forget POST request
25847
+ this.fetch.call(win, url, {
25848
+ 'method': 'POST',
25849
+ 'headers': {
25850
+ 'Content-Type': 'application/json',
25851
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
25852
+ 'traceparent': traceparent
25853
+ },
25854
+ 'body': JSON.stringify(payload)
25855
+ }).catch(function(error) {
25856
+ // Silent failure - cohort sync will catch up
25857
+ logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
25858
+ });
25859
+ };
25860
+
24880
25861
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
24881
25862
  if (!this.fetchPromise) {
24882
25863
  return new Promise(function(resolve) {
@@ -24995,6 +25976,9 @@ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.up
24995
25976
  // Deprecated method
24996
25977
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
24997
25978
 
25979
+ // Exports intended only for testing
25980
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
25981
+
24998
25982
  /* eslint camelcase: "off" */
24999
25983
 
25000
25984
 
@@ -26464,6 +27448,7 @@ var DEFAULT_CONFIG = {
26464
27448
  'record_min_ms': 0,
26465
27449
  'record_sessions_percent': 0,
26466
27450
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
27451
+ 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
26467
27452
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
26468
27453
  };
26469
27454
 
@@ -26693,7 +27678,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
26693
27678
  getConfigFunc: _.bind(this.get_config, this),
26694
27679
  setConfigFunc: _.bind(this.set_config, this),
26695
27680
  getPropertyFunc: _.bind(this.get_property, this),
26696
- trackingFunc: _.bind(this.track, this)
27681
+ trackingFunc: _.bind(this.track, this),
27682
+ loadExtraBundle: load_extra_bundle,
27683
+ targetingSrc: this.get_config('targeting_src')
26697
27684
  });
26698
27685
  this.flags.init();
26699
27686
  this['flags'] = this.flags;
@@ -26789,11 +27776,11 @@ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpane
26789
27776
 
26790
27777
  var loadRecorder = _.bind(function(startNewIfInactive) {
26791
27778
  var handleLoadedRecorder = _.bind(function() {
26792
- this._recorder = this._recorder || new win['__mp_recorder'](this);
27779
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
26793
27780
  this._recorder['resumeRecording'](startNewIfInactive);
26794
27781
  }, this);
26795
27782
 
26796
- if (_.isUndefined(win['__mp_recorder'])) {
27783
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
26797
27784
  load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
26798
27785
  } else {
26799
27786
  handleLoadedRecorder();
@@ -27535,6 +28522,11 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
27535
28522
  send_request_options: options
27536
28523
  }, callback);
27537
28524
 
28525
+ // Check for first-time event matches
28526
+ if (this.flags && this.flags.checkFirstTimeEvents) {
28527
+ this.flags.checkFirstTimeEvents(event_name, properties);
28528
+ }
28529
+
27538
28530
  return ret;
27539
28531
  });
27540
28532