mixpanel-browser 2.73.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/.eslintrc.json +7 -4
- package/.github/workflows/integration-tests.yml +52 -0
- package/.github/workflows/unit-tests.yml +40 -0
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/build.sh +1 -5
- package/dist/mixpanel-core.cjs.d.ts +12 -1
- package/dist/mixpanel-core.cjs.js +432 -34
- package/dist/mixpanel-recorder.js +5364 -684
- 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.d.ts +12 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +432 -34
- package/dist/mixpanel-with-recorder.d.ts +12 -1
- package/dist/mixpanel-with-recorder.js +7889 -2839
- package/dist/mixpanel-with-recorder.min.d.ts +12 -1
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +12 -1
- package/dist/mixpanel.amd.js +8446 -2813
- package/dist/mixpanel.cjs.d.ts +12 -1
- package/dist/mixpanel.cjs.js +8446 -2813
- package/dist/mixpanel.globals.js +432 -34
- package/dist/mixpanel.min.js +182 -173
- package/dist/mixpanel.module.d.ts +12 -1
- package/dist/mixpanel.module.js +8446 -2813
- package/dist/mixpanel.umd.d.ts +12 -1
- package/dist/mixpanel.umd.js +8446 -2813
- package/dist/rrweb-bundled.js +4434 -596
- package/dist/rrweb-compiled.js +5078 -646
- package/package.json +33 -7
- package/rollup.config.mjs +286 -224
- package/src/autocapture/utils.js +15 -7
- package/src/config.js +1 -1
- package/src/flags/index.js +269 -8
- package/src/globals.js +14 -0
- package/src/index.d.ts +12 -1
- package/src/loaders/loader-module.js +1 -0
- package/src/mixpanel-core.js +101 -8
- package/src/recorder/index.js +2 -1
- package/src/recorder/masking.js +197 -0
- package/src/recorder/rrweb-entrypoint.js +2 -1
- package/src/recorder/session-recording.js +43 -4
- package/src/recorder/utils.js +5 -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 +12 -10
- package/testServer.js +51 -7
- package/.github/workflows/tests.yml +0 -25
- /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var Config = {
|
|
4
4
|
DEBUG: false,
|
|
5
|
-
LIB_VERSION: '2.
|
|
5
|
+
LIB_VERSION: '2.75.0'
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
|
|
@@ -593,15 +593,8 @@ _.isArray = nativeIsArray || function(obj) {
|
|
|
593
593
|
return toString.call(obj) === '[object Array]';
|
|
594
594
|
};
|
|
595
595
|
|
|
596
|
-
// from a comment on http://dbj.org/dbj/?p=286
|
|
597
|
-
// fails on only one very rare and deliberate custom object:
|
|
598
|
-
// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
|
|
599
596
|
_.isFunction = function(f) {
|
|
600
|
-
|
|
601
|
-
return /^\s*\bfunction\b/.test(f);
|
|
602
|
-
} catch (x) {
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
597
|
+
return typeof f === 'function';
|
|
605
598
|
};
|
|
606
599
|
|
|
607
600
|
_.isArguments = function(obj) {
|
|
@@ -1504,8 +1497,17 @@ function _storageWrapper(storage, name, is_supported_fn) {
|
|
|
1504
1497
|
};
|
|
1505
1498
|
}
|
|
1506
1499
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1500
|
+
// Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
|
|
1501
|
+
// so create dummy storage wrappers that silently fail as a fallback.
|
|
1502
|
+
var windowLocalStorage = null, windowSessionStorage = null;
|
|
1503
|
+
try {
|
|
1504
|
+
windowLocalStorage = win.localStorage;
|
|
1505
|
+
windowSessionStorage = win.sessionStorage;
|
|
1506
|
+
// eslint-disable-next-line no-empty
|
|
1507
|
+
} catch (_err) {}
|
|
1508
|
+
|
|
1509
|
+
_.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
|
|
1510
|
+
_.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
|
|
1509
1511
|
|
|
1510
1512
|
_.register_event = (function() {
|
|
1511
1513
|
// written by Dean Edwards, 2005
|
|
@@ -2150,6 +2152,16 @@ var isRecordingExpired = function(serializedRecording) {
|
|
|
2150
2152
|
return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
|
|
2151
2153
|
};
|
|
2152
2154
|
|
|
2155
|
+
/**
|
|
2156
|
+
* Shared global window property names used across modules
|
|
2157
|
+
*/
|
|
2158
|
+
|
|
2159
|
+
// Targeting library global (used by flags and targeting modules)
|
|
2160
|
+
var TARGETING_GLOBAL_NAME = '__mp_targeting';
|
|
2161
|
+
|
|
2162
|
+
// Recorder library global (used by recorder and mixpanel-core)
|
|
2163
|
+
var RECORDER_GLOBAL_NAME = '__mp_recorder';
|
|
2164
|
+
|
|
2153
2165
|
// stateless utils
|
|
2154
2166
|
// mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
|
|
2155
2167
|
|
|
@@ -2655,6 +2667,18 @@ function shouldTrackDomEvent(el, ev) {
|
|
|
2655
2667
|
}
|
|
2656
2668
|
}
|
|
2657
2669
|
|
|
2670
|
+
function elementLooksSensitive(el) {
|
|
2671
|
+
var name = (el.name || el.id || '').toString().toLowerCase();
|
|
2672
|
+
if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
|
|
2673
|
+
var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
2674
|
+
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
2675
|
+
return true;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
return false;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2658
2682
|
/*
|
|
2659
2683
|
* Check whether a DOM element should be "tracked" or if it may contain sensitive data
|
|
2660
2684
|
* using a variety of heuristics.
|
|
@@ -2707,13 +2731,8 @@ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)
|
|
|
2707
2731
|
}
|
|
2708
2732
|
}
|
|
2709
2733
|
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
|
|
2713
|
-
var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
2714
|
-
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
2715
|
-
return false;
|
|
2716
|
-
}
|
|
2734
|
+
if (elementLooksSensitive(el)) {
|
|
2735
|
+
return false;
|
|
2717
2736
|
}
|
|
2718
2737
|
|
|
2719
2738
|
return true;
|
|
@@ -3891,14 +3910,62 @@ Autocapture.prototype.stopDeadClickTracking = function() {
|
|
|
3891
3910
|
// TODO integrate error_reporter from mixpanel instance
|
|
3892
3911
|
safewrapClass(Autocapture);
|
|
3893
3912
|
|
|
3894
|
-
|
|
3913
|
+
/**
|
|
3914
|
+
* Get the promise-based targeting loader
|
|
3915
|
+
* @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
|
|
3916
|
+
* @param {string} targetingSrc - URL to targeting bundle
|
|
3917
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
3918
|
+
*/
|
|
3919
|
+
var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
|
|
3920
|
+
// Return existing promise if already initialized or loading
|
|
3921
|
+
if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
|
|
3922
|
+
return win[TARGETING_GLOBAL_NAME];
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// Create loading promise and set it as the global immediately
|
|
3926
|
+
// This makes minified build behavior consistent with dev/CJS builds
|
|
3927
|
+
win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
|
|
3928
|
+
loadExtraBundle(targetingSrc, resolve);
|
|
3929
|
+
}).then(function () {
|
|
3930
|
+
var p = win[TARGETING_GLOBAL_NAME];
|
|
3931
|
+
if (p && typeof p.then === 'function') {
|
|
3932
|
+
return p;
|
|
3933
|
+
}
|
|
3934
|
+
throw new Error('targeting failed to load');
|
|
3935
|
+
}).catch(function (err) {
|
|
3936
|
+
delete win[TARGETING_GLOBAL_NAME];
|
|
3937
|
+
throw err;
|
|
3938
|
+
});
|
|
3895
3939
|
|
|
3940
|
+
return win[TARGETING_GLOBAL_NAME];
|
|
3941
|
+
};
|
|
3942
|
+
|
|
3943
|
+
var logger$3 = console_with_prefix('flags');
|
|
3896
3944
|
var FLAGS_CONFIG_KEY = 'flags';
|
|
3897
3945
|
|
|
3898
3946
|
var CONFIG_CONTEXT = 'context';
|
|
3899
3947
|
var CONFIG_DEFAULTS = {};
|
|
3900
3948
|
CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
|
|
3901
3949
|
|
|
3950
|
+
/**
|
|
3951
|
+
* Generate a unique key for a pending first-time event
|
|
3952
|
+
* @param {string} flagKey - The flag key
|
|
3953
|
+
* @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
|
|
3954
|
+
* @returns {string} Composite key in format "flagKey:firstTimeEventHash"
|
|
3955
|
+
*/
|
|
3956
|
+
var getPendingEventKey = function(flagKey, firstTimeEventHash) {
|
|
3957
|
+
return flagKey + ':' + firstTimeEventHash;
|
|
3958
|
+
};
|
|
3959
|
+
|
|
3960
|
+
/**
|
|
3961
|
+
* Extract the flag key from a pending event key
|
|
3962
|
+
* @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
|
|
3963
|
+
* @returns {string} The flag key portion
|
|
3964
|
+
*/
|
|
3965
|
+
var getFlagKeyFromPendingEventKey = function(eventKey) {
|
|
3966
|
+
return eventKey.split(':')[0];
|
|
3967
|
+
};
|
|
3968
|
+
|
|
3902
3969
|
/**
|
|
3903
3970
|
* FeatureFlagManager: support for Mixpanel's feature flagging product
|
|
3904
3971
|
* @constructor
|
|
@@ -3910,6 +3977,8 @@ var FeatureFlagManager = function(initOptions) {
|
|
|
3910
3977
|
this.setMpConfig = initOptions.setConfigFunc;
|
|
3911
3978
|
this.getMpProperty = initOptions.getPropertyFunc;
|
|
3912
3979
|
this.track = initOptions.trackingFunc;
|
|
3980
|
+
this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
|
|
3981
|
+
this.targetingSrc = initOptions.targetingSrc || '';
|
|
3913
3982
|
};
|
|
3914
3983
|
|
|
3915
3984
|
FeatureFlagManager.prototype.init = function() {
|
|
@@ -3922,6 +3991,8 @@ FeatureFlagManager.prototype.init = function() {
|
|
|
3922
3991
|
this.fetchFlags();
|
|
3923
3992
|
|
|
3924
3993
|
this.trackedFeatures = new Set();
|
|
3994
|
+
this.pendingFirstTimeEvents = {};
|
|
3995
|
+
this.activatedFirstTimeEvents = {};
|
|
3925
3996
|
};
|
|
3926
3997
|
|
|
3927
3998
|
FeatureFlagManager.prototype.getFullConfig = function() {
|
|
@@ -4002,17 +4073,78 @@ FeatureFlagManager.prototype.fetchFlags = function() {
|
|
|
4002
4073
|
throw new Error('No flags in API response');
|
|
4003
4074
|
}
|
|
4004
4075
|
var flags = new Map();
|
|
4076
|
+
var pendingFirstTimeEvents = {};
|
|
4077
|
+
|
|
4078
|
+
// Process flags from response
|
|
4005
4079
|
_.each(responseFlags, function(data, key) {
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4080
|
+
// Check if this flag has any activated first-time events this session
|
|
4081
|
+
var hasActivatedEvent = false;
|
|
4082
|
+
var prefix = key + ':';
|
|
4083
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
4084
|
+
if (eventKey.startsWith(prefix)) {
|
|
4085
|
+
hasActivatedEvent = true;
|
|
4086
|
+
}
|
|
4012
4087
|
});
|
|
4013
|
-
|
|
4088
|
+
|
|
4089
|
+
if (hasActivatedEvent) {
|
|
4090
|
+
// Preserve the activated variant, don't overwrite with server's current variant
|
|
4091
|
+
var currentFlag = this.flags && this.flags.get(key);
|
|
4092
|
+
if (currentFlag) {
|
|
4093
|
+
flags.set(key, currentFlag);
|
|
4094
|
+
}
|
|
4095
|
+
} else {
|
|
4096
|
+
// Use server's current variant
|
|
4097
|
+
flags.set(key, {
|
|
4098
|
+
'key': data['variant_key'],
|
|
4099
|
+
'value': data['variant_value'],
|
|
4100
|
+
'experiment_id': data['experiment_id'],
|
|
4101
|
+
'is_experiment_active': data['is_experiment_active'],
|
|
4102
|
+
'is_qa_tester': data['is_qa_tester']
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
}, this);
|
|
4106
|
+
|
|
4107
|
+
// Process top-level pending_first_time_events array
|
|
4108
|
+
var topLevelDefinitions = responseBody['pending_first_time_events'];
|
|
4109
|
+
if (topLevelDefinitions && topLevelDefinitions.length > 0) {
|
|
4110
|
+
_.each(topLevelDefinitions, function(def) {
|
|
4111
|
+
var flagKey = def['flag_key'];
|
|
4112
|
+
var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
|
|
4113
|
+
|
|
4114
|
+
// Skip if this specific event has already been activated this session
|
|
4115
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// Store pending event definition using composite key
|
|
4120
|
+
pendingFirstTimeEvents[eventKey] = {
|
|
4121
|
+
'flag_key': flagKey,
|
|
4122
|
+
'flag_id': def['flag_id'],
|
|
4123
|
+
'project_id': def['project_id'],
|
|
4124
|
+
'first_time_event_hash': def['first_time_event_hash'],
|
|
4125
|
+
'event_name': def['event_name'],
|
|
4126
|
+
'property_filters': def['property_filters'],
|
|
4127
|
+
'pending_variant': def['pending_variant']
|
|
4128
|
+
};
|
|
4129
|
+
}, this);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
// Preserve any activated orphaned flags (flags that were activated but are no longer in response)
|
|
4133
|
+
if (this.activatedFirstTimeEvents) {
|
|
4134
|
+
_.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
|
|
4135
|
+
var flagKey = getFlagKeyFromPendingEventKey(eventKey);
|
|
4136
|
+
if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
|
|
4137
|
+
// Keep the activated flag even though it's not in the new response
|
|
4138
|
+
flags.set(flagKey, this.flags.get(flagKey));
|
|
4139
|
+
}
|
|
4140
|
+
}, this);
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4014
4143
|
this.flags = flags;
|
|
4144
|
+
this.pendingFirstTimeEvents = pendingFirstTimeEvents;
|
|
4015
4145
|
this._traceparent = traceparent;
|
|
4146
|
+
|
|
4147
|
+
this._loadTargetingIfNeeded();
|
|
4016
4148
|
}.bind(this)).catch(function(error) {
|
|
4017
4149
|
this.markFetchComplete();
|
|
4018
4150
|
logger$3.error(error);
|
|
@@ -4036,6 +4168,177 @@ FeatureFlagManager.prototype.markFetchComplete = function() {
|
|
|
4036
4168
|
this._fetchInProgressStartTime = null;
|
|
4037
4169
|
};
|
|
4038
4170
|
|
|
4171
|
+
/**
|
|
4172
|
+
* Proactively load targeting bundle if any pending events have property filters
|
|
4173
|
+
*/
|
|
4174
|
+
FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
|
|
4175
|
+
var hasPropertyFilters = false;
|
|
4176
|
+
_.each(this.pendingFirstTimeEvents, function(evt) {
|
|
4177
|
+
if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
|
|
4178
|
+
hasPropertyFilters = true;
|
|
4179
|
+
}
|
|
4180
|
+
});
|
|
4181
|
+
|
|
4182
|
+
if (hasPropertyFilters) {
|
|
4183
|
+
this.getTargeting().then(function() {
|
|
4184
|
+
logger$3.log('targeting loaded for property filter evaluation');
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
};
|
|
4188
|
+
|
|
4189
|
+
/**
|
|
4190
|
+
* Get the targeting library (initializes if not already loaded)
|
|
4191
|
+
* This method is primarily for testing - production code should rely on automatic loading
|
|
4192
|
+
* @returns {Promise} Promise that resolves with targeting library
|
|
4193
|
+
*/
|
|
4194
|
+
FeatureFlagManager.prototype.getTargeting = function() {
|
|
4195
|
+
return getTargetingPromise(
|
|
4196
|
+
this.loadExtraBundle.bind(this),
|
|
4197
|
+
this.targetingSrc
|
|
4198
|
+
).catch(function(error) {
|
|
4199
|
+
logger$3.error('Failed to load targeting: ' + error);
|
|
4200
|
+
}.bind(this));
|
|
4201
|
+
};
|
|
4202
|
+
|
|
4203
|
+
/**
|
|
4204
|
+
* Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
|
|
4205
|
+
* @param {string} eventName - The name of the event being tracked
|
|
4206
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
4207
|
+
*
|
|
4208
|
+
* When a match is found (event name matches and property filters pass), this method:
|
|
4209
|
+
* - Switches the flag to the pending variant
|
|
4210
|
+
* - Marks the event as activated for this session
|
|
4211
|
+
* - Records the activation via the API (fire-and-forget)
|
|
4212
|
+
*/
|
|
4213
|
+
FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
|
|
4214
|
+
if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
// Check if targeting promise exists (either bundled or async loaded)
|
|
4219
|
+
if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
|
|
4220
|
+
win[TARGETING_GLOBAL_NAME].then(function(library) {
|
|
4221
|
+
this._processFirstTimeEventCheck(eventName, properties, library);
|
|
4222
|
+
}.bind(this)).catch(function() {
|
|
4223
|
+
// If targeting failed to load, process with null
|
|
4224
|
+
// Events without property filters will still match
|
|
4225
|
+
this._processFirstTimeEventCheck(eventName, properties, null);
|
|
4226
|
+
}.bind(this));
|
|
4227
|
+
} else {
|
|
4228
|
+
// No targeting available, process with null
|
|
4229
|
+
// Events without property filters will still match
|
|
4230
|
+
this._processFirstTimeEventCheck(eventName, properties, null);
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
|
|
4234
|
+
/**
|
|
4235
|
+
* Internal method to process first-time event checks with loaded targeting library
|
|
4236
|
+
* @param {string} eventName - The name of the event being tracked
|
|
4237
|
+
* @param {Object} properties - Event properties to evaluate against property filters
|
|
4238
|
+
* @param {Object} targeting - The loaded targeting library
|
|
4239
|
+
*/
|
|
4240
|
+
FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
|
|
4241
|
+
_.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
|
|
4242
|
+
if (this.activatedFirstTimeEvents[eventKey]) {
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
var flagKey = pendingEvent['flag_key'];
|
|
4247
|
+
|
|
4248
|
+
// Use targeting module to check if event matches
|
|
4249
|
+
var matchResult;
|
|
4250
|
+
|
|
4251
|
+
// If no targeting library and event has property filters, skip it
|
|
4252
|
+
if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
|
|
4253
|
+
logger$3.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
// For simple events (no property filters), just check event name
|
|
4258
|
+
if (!targeting) {
|
|
4259
|
+
matchResult = {
|
|
4260
|
+
matches: eventName === pendingEvent['event_name'],
|
|
4261
|
+
error: null
|
|
4262
|
+
};
|
|
4263
|
+
} else {
|
|
4264
|
+
var criteria = {
|
|
4265
|
+
'event_name': pendingEvent['event_name'],
|
|
4266
|
+
'property_filters': pendingEvent['property_filters']
|
|
4267
|
+
};
|
|
4268
|
+
matchResult = targeting['eventMatchesCriteria'](
|
|
4269
|
+
eventName,
|
|
4270
|
+
properties,
|
|
4271
|
+
criteria
|
|
4272
|
+
);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
if (matchResult.error) {
|
|
4276
|
+
logger$3.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
if (!matchResult.matches) {
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
logger$3.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
|
|
4285
|
+
|
|
4286
|
+
var newVariant = {
|
|
4287
|
+
'key': pendingEvent['pending_variant']['variant_key'],
|
|
4288
|
+
'value': pendingEvent['pending_variant']['variant_value'],
|
|
4289
|
+
'experiment_id': pendingEvent['pending_variant']['experiment_id'],
|
|
4290
|
+
'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
|
|
4291
|
+
};
|
|
4292
|
+
|
|
4293
|
+
this.flags.set(flagKey, newVariant);
|
|
4294
|
+
this.activatedFirstTimeEvents[eventKey] = true;
|
|
4295
|
+
|
|
4296
|
+
this.recordFirstTimeEvent(
|
|
4297
|
+
pendingEvent['flag_id'],
|
|
4298
|
+
pendingEvent['project_id'],
|
|
4299
|
+
pendingEvent['first_time_event_hash']
|
|
4300
|
+
);
|
|
4301
|
+
}, this);
|
|
4302
|
+
};
|
|
4303
|
+
|
|
4304
|
+
FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
|
|
4305
|
+
// Construct URL: {api_host}/flags/{flagId}/first-time-events
|
|
4306
|
+
return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
|
|
4307
|
+
};
|
|
4308
|
+
|
|
4309
|
+
FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
|
|
4310
|
+
var distinctId = this.getMpProperty('distinct_id');
|
|
4311
|
+
var traceparent = generateTraceparent();
|
|
4312
|
+
|
|
4313
|
+
// Build URL with query string parameters
|
|
4314
|
+
var searchParams = new URLSearchParams();
|
|
4315
|
+
searchParams.set('mp_lib', 'web');
|
|
4316
|
+
searchParams.set('$lib_version', Config.LIB_VERSION);
|
|
4317
|
+
var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
|
|
4318
|
+
|
|
4319
|
+
var payload = {
|
|
4320
|
+
'distinct_id': distinctId,
|
|
4321
|
+
'project_id': projectId,
|
|
4322
|
+
'first_time_event_hash': firstTimeEventHash
|
|
4323
|
+
};
|
|
4324
|
+
|
|
4325
|
+
logger$3.log('Recording first-time event for flag: ' + flagId);
|
|
4326
|
+
|
|
4327
|
+
// Fire-and-forget POST request
|
|
4328
|
+
this.fetch.call(win, url, {
|
|
4329
|
+
'method': 'POST',
|
|
4330
|
+
'headers': {
|
|
4331
|
+
'Content-Type': 'application/json',
|
|
4332
|
+
'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
|
|
4333
|
+
'traceparent': traceparent
|
|
4334
|
+
},
|
|
4335
|
+
'body': JSON.stringify(payload)
|
|
4336
|
+
}).catch(function(error) {
|
|
4337
|
+
// Silent failure - cohort sync will catch up
|
|
4338
|
+
logger$3.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
|
|
4339
|
+
});
|
|
4340
|
+
};
|
|
4341
|
+
|
|
4039
4342
|
FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
|
|
4040
4343
|
if (!this.fetchPromise) {
|
|
4041
4344
|
return new Promise(function(resolve) {
|
|
@@ -4154,6 +4457,9 @@ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.up
|
|
|
4154
4457
|
// Deprecated method
|
|
4155
4458
|
FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
|
|
4156
4459
|
|
|
4460
|
+
// Exports intended only for testing
|
|
4461
|
+
FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
|
|
4462
|
+
|
|
4157
4463
|
/* eslint camelcase: "off" */
|
|
4158
4464
|
|
|
4159
4465
|
|
|
@@ -6841,6 +7147,9 @@ var INIT_SNIPPET = 1;
|
|
|
6841
7147
|
/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
|
|
6842
7148
|
/** @const */ var PAYLOAD_TYPE_JSON = 'json';
|
|
6843
7149
|
/** @const */ var DEVICE_ID_PREFIX = '$device:';
|
|
7150
|
+
/** @const */ var SETTING_STRICT = 'strict';
|
|
7151
|
+
/** @const */ var SETTING_FALLBACK = 'fallback';
|
|
7152
|
+
/** @const */ var SETTING_DISABLED = 'disabled';
|
|
6844
7153
|
|
|
6845
7154
|
|
|
6846
7155
|
/*
|
|
@@ -6869,7 +7178,8 @@ var DEFAULT_API_ROUTES = {
|
|
|
6869
7178
|
'engage': 'engage/',
|
|
6870
7179
|
'groups': 'groups/',
|
|
6871
7180
|
'record': 'record/',
|
|
6872
|
-
'flags': 'flags/'
|
|
7181
|
+
'flags': 'flags/',
|
|
7182
|
+
'settings': 'settings/'
|
|
6873
7183
|
};
|
|
6874
7184
|
|
|
6875
7185
|
/*
|
|
@@ -6933,12 +7243,13 @@ var DEFAULT_CONFIG = {
|
|
|
6933
7243
|
'record_console': true,
|
|
6934
7244
|
'record_heatmap_data': false,
|
|
6935
7245
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
6936
|
-
'
|
|
6937
|
-
'record_mask_text_selector': '*',
|
|
7246
|
+
'record_mask_inputs': true,
|
|
6938
7247
|
'record_max_ms': MAX_RECORDING_MS,
|
|
6939
7248
|
'record_min_ms': 0,
|
|
6940
7249
|
'record_sessions_percent': 0,
|
|
6941
|
-
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
|
|
7250
|
+
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
|
|
7251
|
+
'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
|
|
7252
|
+
'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
|
|
6942
7253
|
};
|
|
6943
7254
|
|
|
6944
7255
|
var DOM_LOADED = false;
|
|
@@ -7167,7 +7478,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
7167
7478
|
getConfigFunc: _.bind(this.get_config, this),
|
|
7168
7479
|
setConfigFunc: _.bind(this.set_config, this),
|
|
7169
7480
|
getPropertyFunc: _.bind(this.get_property, this),
|
|
7170
|
-
trackingFunc: _.bind(this.track, this)
|
|
7481
|
+
trackingFunc: _.bind(this.track, this),
|
|
7482
|
+
loadExtraBundle: load_extra_bundle,
|
|
7483
|
+
targetingSrc: this.get_config('targeting_src')
|
|
7171
7484
|
});
|
|
7172
7485
|
this.flags.init();
|
|
7173
7486
|
this['flags'] = this.flags;
|
|
@@ -7176,7 +7489,16 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
7176
7489
|
this.autocapture.init();
|
|
7177
7490
|
|
|
7178
7491
|
this._init_tab_id();
|
|
7179
|
-
|
|
7492
|
+
|
|
7493
|
+
// Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
|
|
7494
|
+
var mode = this.get_config('remote_settings_mode');
|
|
7495
|
+
if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
|
|
7496
|
+
this._fetch_remote_settings(mode).then(_.bind(function() {
|
|
7497
|
+
this._check_and_start_session_recording();
|
|
7498
|
+
}, this));
|
|
7499
|
+
} else {
|
|
7500
|
+
this._check_and_start_session_recording();
|
|
7501
|
+
}
|
|
7180
7502
|
};
|
|
7181
7503
|
|
|
7182
7504
|
/**
|
|
@@ -7254,11 +7576,11 @@ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpane
|
|
|
7254
7576
|
|
|
7255
7577
|
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
7256
7578
|
var handleLoadedRecorder = _.bind(function() {
|
|
7257
|
-
this._recorder = this._recorder || new win[
|
|
7579
|
+
this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
|
|
7258
7580
|
this._recorder['resumeRecording'](startNewIfInactive);
|
|
7259
7581
|
}, this);
|
|
7260
7582
|
|
|
7261
|
-
if (_.isUndefined(win[
|
|
7583
|
+
if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
|
|
7262
7584
|
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
7263
7585
|
} else {
|
|
7264
7586
|
handleLoadedRecorder();
|
|
@@ -7601,6 +7923,77 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
|
|
|
7601
7923
|
return succeeded;
|
|
7602
7924
|
};
|
|
7603
7925
|
|
|
7926
|
+
MixpanelLib.prototype._fetch_remote_settings = function(mode) {
|
|
7927
|
+
var disableRecordingIfStrict = function() {
|
|
7928
|
+
if (mode === 'strict') {
|
|
7929
|
+
self.set_config({'record_sessions_percent': 0});
|
|
7930
|
+
}
|
|
7931
|
+
};
|
|
7932
|
+
|
|
7933
|
+
if (!win['AbortController']) {
|
|
7934
|
+
console.critical('Remote settings unavailable: missing minimum required APIs');
|
|
7935
|
+
disableRecordingIfStrict();
|
|
7936
|
+
return Promise.resolve();
|
|
7937
|
+
}
|
|
7938
|
+
|
|
7939
|
+
var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
|
|
7940
|
+
var request_params = {
|
|
7941
|
+
'$lib_version': Config.LIB_VERSION,
|
|
7942
|
+
'mp_lib': 'web',
|
|
7943
|
+
'sdk_config': '1',
|
|
7944
|
+
};
|
|
7945
|
+
var query_string = _.HTTPBuildQuery(request_params);
|
|
7946
|
+
var full_url = settings_endpoint + '?' + query_string;
|
|
7947
|
+
var self = this;
|
|
7948
|
+
|
|
7949
|
+
var abortController = new AbortController();
|
|
7950
|
+
var timeout_id = setTimeout(function() {
|
|
7951
|
+
abortController.abort();
|
|
7952
|
+
}, 500);
|
|
7953
|
+
var fetchOptions = {
|
|
7954
|
+
'method': 'GET',
|
|
7955
|
+
'headers': {
|
|
7956
|
+
'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
|
|
7957
|
+
},
|
|
7958
|
+
'signal': abortController.signal
|
|
7959
|
+
};
|
|
7960
|
+
|
|
7961
|
+
return win['fetch'](full_url, fetchOptions).then(function(response) {
|
|
7962
|
+
clearTimeout(timeout_id);
|
|
7963
|
+
if (!response['ok']) {
|
|
7964
|
+
console.critical('Network response was not ok');
|
|
7965
|
+
disableRecordingIfStrict();
|
|
7966
|
+
return;
|
|
7967
|
+
}
|
|
7968
|
+
return response.json();
|
|
7969
|
+
}).then(function(result) {
|
|
7970
|
+
if (result && result['sdk_config'] && result['sdk_config']['config']) {
|
|
7971
|
+
var remote_config = result['sdk_config']['config'];
|
|
7972
|
+
|
|
7973
|
+
// Verify that remote config contains only valid keys from DEFAULT_CONFIG
|
|
7974
|
+
var valid_config = {};
|
|
7975
|
+
_.each(remote_config, function(value, key) {
|
|
7976
|
+
if (DEFAULT_CONFIG.hasOwnProperty(key)) {
|
|
7977
|
+
valid_config[key] = value;
|
|
7978
|
+
}
|
|
7979
|
+
});
|
|
7980
|
+
|
|
7981
|
+
if (_.isEmptyObject(valid_config)) {
|
|
7982
|
+
console.critical('No valid config keys found in remote settings.');
|
|
7983
|
+
disableRecordingIfStrict();
|
|
7984
|
+
} else {
|
|
7985
|
+
self.set_config(valid_config);
|
|
7986
|
+
}
|
|
7987
|
+
} else {
|
|
7988
|
+
disableRecordingIfStrict();
|
|
7989
|
+
}
|
|
7990
|
+
}).catch(function(err) {
|
|
7991
|
+
clearTimeout(timeout_id);
|
|
7992
|
+
console.critical('Failed to fetch remote settings', err);
|
|
7993
|
+
disableRecordingIfStrict();
|
|
7994
|
+
});
|
|
7995
|
+
};
|
|
7996
|
+
|
|
7604
7997
|
/**
|
|
7605
7998
|
* _execute_array() deals with processing any mixpanel function
|
|
7606
7999
|
* calls that were called before the Mixpanel library were loaded
|
|
@@ -7929,6 +8322,11 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
|
|
|
7929
8322
|
send_request_options: options
|
|
7930
8323
|
}, callback);
|
|
7931
8324
|
|
|
8325
|
+
// Check for first-time event matches
|
|
8326
|
+
if (this.flags && this.flags.checkFirstTimeEvents) {
|
|
8327
|
+
this.flags.checkFirstTimeEvents(event_name, properties);
|
|
8328
|
+
}
|
|
8329
|
+
|
|
7932
8330
|
return ret;
|
|
7933
8331
|
});
|
|
7934
8332
|
|