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.
- package/.github/workflows/unit-tests.yml +1 -1
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/dist/mixpanel-core.cjs.js +318 -20
- package/dist/mixpanel-recorder.js +127 -15
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +2576 -0
- package/dist/mixpanel-targeting.min.js +2 -0
- package/dist/mixpanel-targeting.min.js.map +1 -0
- package/dist/mixpanel-with-async-modules.cjs.d.ts +522 -0
- package/dist/mixpanel-with-async-modules.cjs.js +9700 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +318 -20
- package/dist/mixpanel-with-recorder.js +435 -26
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.js +1020 -28
- package/dist/mixpanel.cjs.js +1020 -28
- package/dist/mixpanel.globals.js +318 -20
- package/dist/mixpanel.min.js +179 -172
- package/dist/mixpanel.module.js +1020 -28
- package/dist/mixpanel.umd.js +1020 -28
- package/dist/rrweb-bundled.js +119 -5
- package/dist/rrweb-compiled.js +116 -5
- package/package.json +4 -3
- package/rollup.config.mjs +34 -2
- package/src/config.js +1 -1
- package/src/flags/index.js +269 -8
- package/src/globals.js +14 -0
- package/src/loaders/loader-module.js +1 -0
- package/src/mixpanel-core.js +12 -3
- package/src/recorder/index.js +2 -1
- package/src/targeting/event-matcher.js +97 -0
- package/src/targeting/index.js +11 -0
- package/src/targeting/loader.js +36 -0
- package/src/utils.js +1 -8
- package/.claude/settings.local.json +0 -12
- /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
package/src/flags/index.js
CHANGED
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
};
|
package/src/mixpanel-core.js
CHANGED
|
@@ -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[
|
|
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[
|
|
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
|
|
package/src/recorder/index.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
/package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js}
RENAMED
|
File without changes
|