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