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