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
@@ -1,15 +1,37 @@
1
1
  import { _, console_with_prefix, generateTraceparent, safewrapClass } from '../utils'; // eslint-disable-line camelcase
2
2
  import { window } from '../window';
3
3
  import Config from '../config';
4
+ import {
5
+ getTargetingPromise
6
+ } from '../targeting/loader';
7
+ import { TARGETING_GLOBAL_NAME } from '../globals';
4
8
 
5
9
  var logger = console_with_prefix('flags');
6
-
7
10
  var FLAGS_CONFIG_KEY = 'flags';
8
11
 
9
12
  var CONFIG_CONTEXT = 'context';
10
13
  var CONFIG_DEFAULTS = {};
11
14
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
12
15
 
16
+ /**
17
+ * Generate a unique key for a pending first-time event
18
+ * @param {string} flagKey - The flag key
19
+ * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
20
+ * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
21
+ */
22
+ var getPendingEventKey = function(flagKey, firstTimeEventHash) {
23
+ return flagKey + ':' + firstTimeEventHash;
24
+ };
25
+
26
+ /**
27
+ * Extract the flag key from a pending event key
28
+ * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
29
+ * @returns {string} The flag key portion
30
+ */
31
+ var getFlagKeyFromPendingEventKey = function(eventKey) {
32
+ return eventKey.split(':')[0];
33
+ };
34
+
13
35
  /**
14
36
  * FeatureFlagManager: support for Mixpanel's feature flagging product
15
37
  * @constructor
@@ -21,6 +43,8 @@ var FeatureFlagManager = function(initOptions) {
21
43
  this.setMpConfig = initOptions.setConfigFunc;
22
44
  this.getMpProperty = initOptions.getPropertyFunc;
23
45
  this.track = initOptions.trackingFunc;
46
+ this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
47
+ this.targetingSrc = initOptions.targetingSrc || '';
24
48
  };
25
49
 
26
50
  FeatureFlagManager.prototype.init = function() {
@@ -33,6 +57,8 @@ FeatureFlagManager.prototype.init = function() {
33
57
  this.fetchFlags();
34
58
 
35
59
  this.trackedFeatures = new Set();
60
+ this.pendingFirstTimeEvents = {};
61
+ this.activatedFirstTimeEvents = {};
36
62
  };
37
63
 
38
64
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -113,17 +139,78 @@ FeatureFlagManager.prototype.fetchFlags = function() {
113
139
  throw new Error('No flags in API response');
114
140
  }
115
141
  var flags = new Map();
142
+ var pendingFirstTimeEvents = {};
143
+
144
+ // Process flags from response
116
145
  _.each(responseFlags, function(data, key) {
117
- flags.set(key, {
118
- 'key': data['variant_key'],
119
- 'value': data['variant_value'],
120
- 'experiment_id': data['experiment_id'],
121
- 'is_experiment_active': data['is_experiment_active'],
122
- 'is_qa_tester': data['is_qa_tester']
146
+ // Check if this flag has any activated first-time events this session
147
+ var hasActivatedEvent = false;
148
+ var prefix = key + ':';
149
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
150
+ if (eventKey.startsWith(prefix)) {
151
+ hasActivatedEvent = true;
152
+ }
123
153
  });
124
- });
154
+
155
+ if (hasActivatedEvent) {
156
+ // Preserve the activated variant, don't overwrite with server's current variant
157
+ var currentFlag = this.flags && this.flags.get(key);
158
+ if (currentFlag) {
159
+ flags.set(key, currentFlag);
160
+ }
161
+ } else {
162
+ // Use server's current variant
163
+ flags.set(key, {
164
+ 'key': data['variant_key'],
165
+ 'value': data['variant_value'],
166
+ 'experiment_id': data['experiment_id'],
167
+ 'is_experiment_active': data['is_experiment_active'],
168
+ 'is_qa_tester': data['is_qa_tester']
169
+ });
170
+ }
171
+ }, this);
172
+
173
+ // Process top-level pending_first_time_events array
174
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
175
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
176
+ _.each(topLevelDefinitions, function(def) {
177
+ var flagKey = def['flag_key'];
178
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
179
+
180
+ // Skip if this specific event has already been activated this session
181
+ if (this.activatedFirstTimeEvents[eventKey]) {
182
+ return;
183
+ }
184
+
185
+ // Store pending event definition using composite key
186
+ pendingFirstTimeEvents[eventKey] = {
187
+ 'flag_key': flagKey,
188
+ 'flag_id': def['flag_id'],
189
+ 'project_id': def['project_id'],
190
+ 'first_time_event_hash': def['first_time_event_hash'],
191
+ 'event_name': def['event_name'],
192
+ 'property_filters': def['property_filters'],
193
+ 'pending_variant': def['pending_variant']
194
+ };
195
+ }, this);
196
+ }
197
+
198
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
199
+ if (this.activatedFirstTimeEvents) {
200
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
201
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
202
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
203
+ // Keep the activated flag even though it's not in the new response
204
+ flags.set(flagKey, this.flags.get(flagKey));
205
+ }
206
+ }, this);
207
+ }
208
+
125
209
  this.flags = flags;
210
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
126
211
  this._traceparent = traceparent;
212
+
213
+ this._loadTargetingIfNeeded();
127
214
  }.bind(this)).catch(function(error) {
128
215
  this.markFetchComplete();
129
216
  logger.error(error);
@@ -147,6 +234,177 @@ FeatureFlagManager.prototype.markFetchComplete = function() {
147
234
  this._fetchInProgressStartTime = null;
148
235
  };
149
236
 
237
+ /**
238
+ * Proactively load targeting bundle if any pending events have property filters
239
+ */
240
+ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
241
+ var hasPropertyFilters = false;
242
+ _.each(this.pendingFirstTimeEvents, function(evt) {
243
+ if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
244
+ hasPropertyFilters = true;
245
+ }
246
+ });
247
+
248
+ if (hasPropertyFilters) {
249
+ this.getTargeting().then(function() {
250
+ logger.log('targeting loaded for property filter evaluation');
251
+ });
252
+ }
253
+ };
254
+
255
+ /**
256
+ * Get the targeting library (initializes if not already loaded)
257
+ * This method is primarily for testing - production code should rely on automatic loading
258
+ * @returns {Promise} Promise that resolves with targeting library
259
+ */
260
+ FeatureFlagManager.prototype.getTargeting = function() {
261
+ return getTargetingPromise(
262
+ this.loadExtraBundle.bind(this),
263
+ this.targetingSrc
264
+ ).catch(function(error) {
265
+ logger.error('Failed to load targeting: ' + error);
266
+ }.bind(this));
267
+ };
268
+
269
+ /**
270
+ * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
271
+ * @param {string} eventName - The name of the event being tracked
272
+ * @param {Object} properties - Event properties to evaluate against property filters
273
+ *
274
+ * When a match is found (event name matches and property filters pass), this method:
275
+ * - Switches the flag to the pending variant
276
+ * - Marks the event as activated for this session
277
+ * - Records the activation via the API (fire-and-forget)
278
+ */
279
+ FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
280
+ if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
281
+ return;
282
+ }
283
+
284
+ // Check if targeting promise exists (either bundled or async loaded)
285
+ if (window[TARGETING_GLOBAL_NAME] && _.isFunction(window[TARGETING_GLOBAL_NAME].then)) {
286
+ window[TARGETING_GLOBAL_NAME].then(function(library) {
287
+ this._processFirstTimeEventCheck(eventName, properties, library);
288
+ }.bind(this)).catch(function() {
289
+ // If targeting failed to load, process with null
290
+ // Events without property filters will still match
291
+ this._processFirstTimeEventCheck(eventName, properties, null);
292
+ }.bind(this));
293
+ } else {
294
+ // No targeting available, process with null
295
+ // Events without property filters will still match
296
+ this._processFirstTimeEventCheck(eventName, properties, null);
297
+ }
298
+ };
299
+
300
+ /**
301
+ * Internal method to process first-time event checks with loaded targeting library
302
+ * @param {string} eventName - The name of the event being tracked
303
+ * @param {Object} properties - Event properties to evaluate against property filters
304
+ * @param {Object} targeting - The loaded targeting library
305
+ */
306
+ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
307
+ _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
308
+ if (this.activatedFirstTimeEvents[eventKey]) {
309
+ return;
310
+ }
311
+
312
+ var flagKey = pendingEvent['flag_key'];
313
+
314
+ // Use targeting module to check if event matches
315
+ var matchResult;
316
+
317
+ // If no targeting library and event has property filters, skip it
318
+ if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
319
+ logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
320
+ return;
321
+ }
322
+
323
+ // For simple events (no property filters), just check event name
324
+ if (!targeting) {
325
+ matchResult = {
326
+ matches: eventName === pendingEvent['event_name'],
327
+ error: null
328
+ };
329
+ } else {
330
+ var criteria = {
331
+ 'event_name': pendingEvent['event_name'],
332
+ 'property_filters': pendingEvent['property_filters']
333
+ };
334
+ matchResult = targeting['eventMatchesCriteria'](
335
+ eventName,
336
+ properties,
337
+ criteria
338
+ );
339
+ }
340
+
341
+ if (matchResult.error) {
342
+ logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
343
+ return;
344
+ }
345
+
346
+ if (!matchResult.matches) {
347
+ return;
348
+ }
349
+
350
+ logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
351
+
352
+ var newVariant = {
353
+ 'key': pendingEvent['pending_variant']['variant_key'],
354
+ 'value': pendingEvent['pending_variant']['variant_value'],
355
+ 'experiment_id': pendingEvent['pending_variant']['experiment_id'],
356
+ 'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
357
+ };
358
+
359
+ this.flags.set(flagKey, newVariant);
360
+ this.activatedFirstTimeEvents[eventKey] = true;
361
+
362
+ this.recordFirstTimeEvent(
363
+ pendingEvent['flag_id'],
364
+ pendingEvent['project_id'],
365
+ pendingEvent['first_time_event_hash']
366
+ );
367
+ }, this);
368
+ };
369
+
370
+ FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
371
+ // Construct URL: {api_host}/flags/{flagId}/first-time-events
372
+ return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
373
+ };
374
+
375
+ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
376
+ var distinctId = this.getMpProperty('distinct_id');
377
+ var traceparent = generateTraceparent();
378
+
379
+ // Build URL with query string parameters
380
+ var searchParams = new URLSearchParams();
381
+ searchParams.set('mp_lib', 'web');
382
+ searchParams.set('$lib_version', Config.LIB_VERSION);
383
+ var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
384
+
385
+ var payload = {
386
+ 'distinct_id': distinctId,
387
+ 'project_id': projectId,
388
+ 'first_time_event_hash': firstTimeEventHash
389
+ };
390
+
391
+ logger.log('Recording first-time event for flag: ' + flagId);
392
+
393
+ // Fire-and-forget POST request
394
+ this.fetch.call(window, url, {
395
+ 'method': 'POST',
396
+ 'headers': {
397
+ 'Content-Type': 'application/json',
398
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
399
+ 'traceparent': traceparent
400
+ },
401
+ 'body': JSON.stringify(payload)
402
+ }).catch(function(error) {
403
+ // Silent failure - cohort sync will catch up
404
+ logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
405
+ });
406
+ };
407
+
150
408
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
151
409
  if (!this.fetchPromise) {
152
410
  return new Promise(function(resolve) {
@@ -265,4 +523,7 @@ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.up
265
523
  // Deprecated method
266
524
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
267
525
 
526
+ // Exports intended only for testing
527
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
528
+
268
529
  export { FeatureFlagManager };
package/src/globals.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared global window property names used across modules
3
+ */
4
+
5
+ // Targeting library global (used by flags and targeting modules)
6
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
7
+
8
+ // Recorder library global (used by recorder and mixpanel-core)
9
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
10
+
11
+ export {
12
+ TARGETING_GLOBAL_NAME,
13
+ RECORDER_GLOBAL_NAME
14
+ };
@@ -1,5 +1,6 @@
1
1
  /* eslint camelcase: "off" */
2
2
  import '../recorder';
3
+ import '../targeting';
3
4
 
4
5
  import { init_as_module } from '../mixpanel-core';
5
6
  import { loadNoop } from './bundle-loaders';
@@ -3,6 +3,7 @@ import Config from './config';
3
3
  import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice, NOOP_FUNC, JSONStringify } from './utils';
4
4
  import { isRecordingExpired } from './recorder/utils';
5
5
  import { window } from './window';
6
+ import { RECORDER_GLOBAL_NAME } from './globals';
6
7
  import { Autocapture } from './autocapture';
7
8
  import { FeatureFlagManager } from './flags';
8
9
  import { FormTracker, LinkTracker } from './dom-trackers';
@@ -162,6 +163,7 @@ var DEFAULT_CONFIG = {
162
163
  'record_min_ms': 0,
163
164
  'record_sessions_percent': 0,
164
165
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
166
+ 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
165
167
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
166
168
  };
167
169
 
@@ -391,7 +393,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
391
393
  getConfigFunc: _.bind(this.get_config, this),
392
394
  setConfigFunc: _.bind(this.set_config, this),
393
395
  getPropertyFunc: _.bind(this.get_property, this),
394
- trackingFunc: _.bind(this.track, this)
396
+ trackingFunc: _.bind(this.track, this),
397
+ loadExtraBundle: load_extra_bundle,
398
+ targetingSrc: this.get_config('targeting_src')
395
399
  });
396
400
  this.flags.init();
397
401
  this['flags'] = this.flags;
@@ -487,11 +491,11 @@ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpane
487
491
 
488
492
  var loadRecorder = _.bind(function(startNewIfInactive) {
489
493
  var handleLoadedRecorder = _.bind(function() {
490
- this._recorder = this._recorder || new window['__mp_recorder'](this);
494
+ this._recorder = this._recorder || new window[RECORDER_GLOBAL_NAME](this);
491
495
  this._recorder['resumeRecording'](startNewIfInactive);
492
496
  }, this);
493
497
 
494
- if (_.isUndefined(window['__mp_recorder'])) {
498
+ if (_.isUndefined(window[RECORDER_GLOBAL_NAME])) {
495
499
  load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
496
500
  } else {
497
501
  handleLoadedRecorder();
@@ -1233,6 +1237,11 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
1233
1237
  send_request_options: options
1234
1238
  }, callback);
1235
1239
 
1240
+ // Check for first-time event matches
1241
+ if (this.flags && this.flags.checkFirstTimeEvents) {
1242
+ this.flags.checkFirstTimeEvents(event_name, properties);
1243
+ }
1244
+
1236
1245
  return ret;
1237
1246
  });
1238
1247
 
@@ -1,4 +1,5 @@
1
1
  import { window } from '../window';
2
+ import { RECORDER_GLOBAL_NAME } from '../globals';
2
3
  import { MixpanelRecorder } from './recorder';
3
4
 
4
- window['__mp_recorder'] = MixpanelRecorder;
5
+ window[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
@@ -0,0 +1,97 @@
1
+ import { _ } from '../utils';
2
+ import jsonLogic from 'json-logic-js';
3
+
4
+ /**
5
+ * Shared helper to recursively lowercase strings in nested structures
6
+ * @param {*} obj - Value to process
7
+ * @param {boolean} lowercaseKeys - Whether to lowercase object keys
8
+ * @returns {*} Processed value with lowercased strings
9
+ */
10
+ var lowercaseJson = function(obj, lowercaseKeys) {
11
+ if (obj === null || obj === undefined) {
12
+ return obj;
13
+ } else if (typeof obj === 'string') {
14
+ return obj.toLowerCase();
15
+ } else if (Array.isArray(obj)) {
16
+ return obj.map(function(item) {
17
+ return lowercaseJson(item, lowercaseKeys);
18
+ });
19
+ } else if (obj === Object(obj)) {
20
+ var result = {};
21
+ for (var key in obj) {
22
+ if (obj.hasOwnProperty(key)) {
23
+ var newKey = lowercaseKeys && typeof key === 'string' ? key.toLowerCase() : key;
24
+ result[newKey] = lowercaseJson(obj[key], lowercaseKeys);
25
+ }
26
+ }
27
+ return result;
28
+ } else {
29
+ return obj;
30
+ }
31
+ };
32
+
33
+ /**
34
+ * Lowercase all string keys and values in a nested structure
35
+ * @param {*} val - Value to process
36
+ * @returns {*} Processed value with lowercased strings
37
+ */
38
+ var lowercaseKeysAndValues = function(val) {
39
+ return lowercaseJson(val, true);
40
+ };
41
+
42
+ /**
43
+ * Lowercase only leaf node string values in a nested structure (keys unchanged)
44
+ * @param {*} val - Value to process
45
+ * @returns {*} Processed value with lowercased leaf strings
46
+ */
47
+ var lowercaseOnlyLeafNodes = function(val) {
48
+ return lowercaseJson(val, false);
49
+ };
50
+
51
+ /**
52
+ * Check if an event matches the given criteria
53
+ * @param {string} eventName - The name of the event being checked
54
+ * @param {Object} properties - Event properties to evaluate against property filters
55
+ * @param {Object} criteria - Criteria to match against, with:
56
+ * - event_name: string - Required event name (case-sensitive match)
57
+ * - property_filters: Object - Optional JsonLogic filters for properties
58
+ * @returns {Object} Result object with:
59
+ * - matches: boolean - Whether the event matches the criteria
60
+ * - error: string|undefined - Error message if evaluation failed
61
+ */
62
+ var eventMatchesCriteria = function(eventName, properties, criteria) {
63
+ // Check exact event name match (case-sensitive)
64
+ if (eventName !== criteria.event_name) {
65
+ return { matches: false };
66
+ }
67
+
68
+ // Evaluate property filters using JsonLogic
69
+ var propertyFilters = criteria.property_filters;
70
+ var filtersMatch = true; // default to true if no filters
71
+
72
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
73
+ try {
74
+ // Lowercase all keys and values in event properties for case-insensitive matching
75
+ var lowercasedProperties = lowercaseKeysAndValues(properties || {});
76
+
77
+ // Lowercase only leaf nodes in JsonLogic filters (keep operators intact)
78
+ var lowercasedFilters = lowercaseOnlyLeafNodes(propertyFilters);
79
+
80
+ filtersMatch = jsonLogic.apply(lowercasedFilters, lowercasedProperties);
81
+ } catch (error) {
82
+ return {
83
+ matches: false,
84
+ error: error.toString()
85
+ };
86
+ }
87
+ }
88
+
89
+ return { matches: filtersMatch };
90
+ };
91
+
92
+ export {
93
+ lowercaseJson,
94
+ lowercaseKeysAndValues,
95
+ lowercaseOnlyLeafNodes,
96
+ eventMatchesCriteria
97
+ };
@@ -0,0 +1,11 @@
1
+ import { window } from '../window';
2
+ import { TARGETING_GLOBAL_NAME } from '../globals';
3
+ import { eventMatchesCriteria } from './event-matcher';
4
+
5
+ // Create targeting library object
6
+ var targetingLibrary = {};
7
+ targetingLibrary['eventMatchesCriteria'] = eventMatchesCriteria;
8
+
9
+ // Set global Promise (use bracket notation to prevent minification)
10
+ // This is the ONE AND ONLY global - matches recorder pattern
11
+ window[TARGETING_GLOBAL_NAME] = Promise.resolve(targetingLibrary);
@@ -0,0 +1,36 @@
1
+ import { window } from '../window';
2
+ import { TARGETING_GLOBAL_NAME } from '../globals';
3
+
4
+ /**
5
+ * Get the promise-based targeting loader
6
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
7
+ * @param {string} targetingSrc - URL to targeting bundle
8
+ * @returns {Promise} Promise that resolves with targeting library
9
+ */
10
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
11
+ // Return existing promise if already initialized or loading
12
+ if (window[TARGETING_GLOBAL_NAME] && typeof window[TARGETING_GLOBAL_NAME].then === 'function') {
13
+ return window[TARGETING_GLOBAL_NAME];
14
+ }
15
+
16
+ // Create loading promise and set it as the global immediately
17
+ // This makes minified build behavior consistent with dev/CJS builds
18
+ window[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
19
+ loadExtraBundle(targetingSrc, resolve);
20
+ }).then(function () {
21
+ var p = window[TARGETING_GLOBAL_NAME];
22
+ if (p && typeof p.then === 'function') {
23
+ return p;
24
+ }
25
+ throw new Error('targeting failed to load');
26
+ }).catch(function (err) {
27
+ delete window[TARGETING_GLOBAL_NAME];
28
+ throw err;
29
+ });
30
+
31
+ return window[TARGETING_GLOBAL_NAME];
32
+ };
33
+
34
+ export {
35
+ getTargetingPromise
36
+ };
package/src/utils.js CHANGED
@@ -204,15 +204,8 @@ _.isArray = nativeIsArray || function(obj) {
204
204
  return toString.call(obj) === '[object Array]';
205
205
  };
206
206
 
207
- // from a comment on http://dbj.org/dbj/?p=286
208
- // fails on only one very rare and deliberate custom object:
209
- // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
210
207
  _.isFunction = function(f) {
211
- try {
212
- return /^\s*\bfunction\b/.test(f);
213
- } catch (x) {
214
- return false;
215
- }
208
+ return typeof f === 'function';
216
209
  };
217
210
 
218
211
  _.isArguments = function(obj) {
@@ -1,12 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(mkdir:*)",
5
- "Bash(cat:*)",
6
- "Bash(node --input-type=module -e \"import { expect } from 'chai'; console.log\\('works'\\);\":*)",
7
- "Bash(BABEL_ENV=test node:*)"
8
- ],
9
- "deny": [],
10
- "ask": []
11
- }
12
- }