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