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
package/src/autocapture/utils.js
CHANGED
|
@@ -505,6 +505,18 @@ function shouldTrackDomEvent(el, ev) {
|
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
function elementLooksSensitive(el) {
|
|
509
|
+
var name = (el.name || el.id || '').toString().toLowerCase();
|
|
510
|
+
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"]
|
|
511
|
+
var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
512
|
+
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
508
520
|
/*
|
|
509
521
|
* Check whether a DOM element should be "tracked" or if it may contain sensitive data
|
|
510
522
|
* using a variety of heuristics.
|
|
@@ -557,13 +569,8 @@ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)
|
|
|
557
569
|
}
|
|
558
570
|
}
|
|
559
571
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
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"]
|
|
563
|
-
var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
564
|
-
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
572
|
+
if (elementLooksSensitive(el)) {
|
|
573
|
+
return false;
|
|
567
574
|
}
|
|
568
575
|
|
|
569
576
|
return true;
|
|
@@ -775,6 +782,7 @@ function getClickEventTargetElement(event) {
|
|
|
775
782
|
export {
|
|
776
783
|
EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_INPUT, EV_LOAD,EV_MP_LOCATION_CHANGE, EV_POPSTATE,
|
|
777
784
|
EV_SCROLL, EV_SCROLLEND, EV_SELECT, EV_SUBMIT, EV_TOGGLE, EV_VISIBILITYCHANGE,
|
|
785
|
+
elementLooksSensitive,
|
|
778
786
|
getClickEventComposedPath,
|
|
779
787
|
getClickEventTargetElement,
|
|
780
788
|
getPolyfillScrollEndFunction,
|
package/src/config.js
CHANGED
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/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type PushItem = Array<string | Dict | ((this: Mixpanel) => void)>;
|
|
|
6
6
|
|
|
7
7
|
export type Query = string | Element | Element[];
|
|
8
8
|
|
|
9
|
+
export type RemoteSettingType = "disabled" | "fallback" | "strict";
|
|
10
|
+
|
|
9
11
|
export interface Dict {
|
|
10
12
|
[key: string]: any;
|
|
11
13
|
}
|
|
@@ -166,6 +168,8 @@ export interface Config {
|
|
|
166
168
|
track?: string;
|
|
167
169
|
engage?: string;
|
|
168
170
|
groups?: string;
|
|
171
|
+
record?: string;
|
|
172
|
+
flags?: string;
|
|
169
173
|
};
|
|
170
174
|
api_method: string;
|
|
171
175
|
api_transport: string;
|
|
@@ -225,12 +229,18 @@ export interface Config {
|
|
|
225
229
|
record_idle_timeout_ms: number;
|
|
226
230
|
record_inline_images: boolean;
|
|
227
231
|
record_mask_text_class: string | RegExp;
|
|
228
|
-
record_mask_text_selector: string;
|
|
232
|
+
record_mask_text_selector: string | string[];
|
|
233
|
+
record_unmask_text_selector: string | string[];
|
|
234
|
+
record_mask_all_text: boolean;
|
|
235
|
+
record_mask_input_selector: string | string[];
|
|
236
|
+
record_unmask_input_selector: string | string[];
|
|
237
|
+
record_mask_all_inputs: boolean;
|
|
229
238
|
record_min_ms: number;
|
|
230
239
|
record_max_ms: number;
|
|
231
240
|
record_sessions_percent: number;
|
|
232
241
|
record_canvas: boolean;
|
|
233
242
|
record_heatmap_data: boolean;
|
|
243
|
+
remote_settings_mode: RemoteSettingType;
|
|
234
244
|
hooks: {
|
|
235
245
|
before_identify?: (new_distinct_id: string) => string | null;
|
|
236
246
|
before_register?: (
|
|
@@ -256,6 +266,7 @@ export interface Config {
|
|
|
256
266
|
};
|
|
257
267
|
}
|
|
258
268
|
|
|
269
|
+
|
|
259
270
|
export type VerboseResponse =
|
|
260
271
|
| {
|
|
261
272
|
status: 1;
|
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';
|
|
@@ -61,6 +62,9 @@ var INIT_SNIPPET = 1;
|
|
|
61
62
|
/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
|
|
62
63
|
/** @const */ var PAYLOAD_TYPE_JSON = 'json';
|
|
63
64
|
/** @const */ var DEVICE_ID_PREFIX = '$device:';
|
|
65
|
+
/** @const */ var SETTING_STRICT = 'strict';
|
|
66
|
+
/** @const */ var SETTING_FALLBACK = 'fallback';
|
|
67
|
+
/** @const */ var SETTING_DISABLED = 'disabled';
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
/*
|
|
@@ -89,7 +93,8 @@ var DEFAULT_API_ROUTES = {
|
|
|
89
93
|
'engage': 'engage/',
|
|
90
94
|
'groups': 'groups/',
|
|
91
95
|
'record': 'record/',
|
|
92
|
-
'flags': 'flags/'
|
|
96
|
+
'flags': 'flags/',
|
|
97
|
+
'settings': 'settings/'
|
|
93
98
|
};
|
|
94
99
|
|
|
95
100
|
/*
|
|
@@ -153,12 +158,13 @@ var DEFAULT_CONFIG = {
|
|
|
153
158
|
'record_console': true,
|
|
154
159
|
'record_heatmap_data': false,
|
|
155
160
|
'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
|
|
156
|
-
'
|
|
157
|
-
'record_mask_text_selector': '*',
|
|
161
|
+
'record_mask_inputs': true,
|
|
158
162
|
'record_max_ms': MAX_RECORDING_MS,
|
|
159
163
|
'record_min_ms': 0,
|
|
160
164
|
'record_sessions_percent': 0,
|
|
161
|
-
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
|
|
165
|
+
'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
|
|
166
|
+
'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
|
|
167
|
+
'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
|
|
162
168
|
};
|
|
163
169
|
|
|
164
170
|
var DOM_LOADED = false;
|
|
@@ -387,7 +393,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
387
393
|
getConfigFunc: _.bind(this.get_config, this),
|
|
388
394
|
setConfigFunc: _.bind(this.set_config, this),
|
|
389
395
|
getPropertyFunc: _.bind(this.get_property, this),
|
|
390
|
-
trackingFunc: _.bind(this.track, this)
|
|
396
|
+
trackingFunc: _.bind(this.track, this),
|
|
397
|
+
loadExtraBundle: load_extra_bundle,
|
|
398
|
+
targetingSrc: this.get_config('targeting_src')
|
|
391
399
|
});
|
|
392
400
|
this.flags.init();
|
|
393
401
|
this['flags'] = this.flags;
|
|
@@ -396,7 +404,16 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
396
404
|
this.autocapture.init();
|
|
397
405
|
|
|
398
406
|
this._init_tab_id();
|
|
399
|
-
|
|
407
|
+
|
|
408
|
+
// Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
|
|
409
|
+
var mode = this.get_config('remote_settings_mode');
|
|
410
|
+
if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
|
|
411
|
+
this._fetch_remote_settings(mode).then(_.bind(function() {
|
|
412
|
+
this._check_and_start_session_recording();
|
|
413
|
+
}, this));
|
|
414
|
+
} else {
|
|
415
|
+
this._check_and_start_session_recording();
|
|
416
|
+
}
|
|
400
417
|
};
|
|
401
418
|
|
|
402
419
|
/**
|
|
@@ -474,11 +491,11 @@ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpane
|
|
|
474
491
|
|
|
475
492
|
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
476
493
|
var handleLoadedRecorder = _.bind(function() {
|
|
477
|
-
this._recorder = this._recorder || new window[
|
|
494
|
+
this._recorder = this._recorder || new window[RECORDER_GLOBAL_NAME](this);
|
|
478
495
|
this._recorder['resumeRecording'](startNewIfInactive);
|
|
479
496
|
}, this);
|
|
480
497
|
|
|
481
|
-
if (_.isUndefined(window[
|
|
498
|
+
if (_.isUndefined(window[RECORDER_GLOBAL_NAME])) {
|
|
482
499
|
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
483
500
|
} else {
|
|
484
501
|
handleLoadedRecorder();
|
|
@@ -821,6 +838,77 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
|
|
|
821
838
|
return succeeded;
|
|
822
839
|
};
|
|
823
840
|
|
|
841
|
+
MixpanelLib.prototype._fetch_remote_settings = function(mode) {
|
|
842
|
+
var disableRecordingIfStrict = function() {
|
|
843
|
+
if (mode === 'strict') {
|
|
844
|
+
self.set_config({'record_sessions_percent': 0});
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
if (!window['AbortController']) {
|
|
849
|
+
console.critical('Remote settings unavailable: missing minimum required APIs');
|
|
850
|
+
disableRecordingIfStrict();
|
|
851
|
+
return Promise.resolve();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
|
|
855
|
+
var request_params = {
|
|
856
|
+
'$lib_version': Config.LIB_VERSION,
|
|
857
|
+
'mp_lib': 'web',
|
|
858
|
+
'sdk_config': '1',
|
|
859
|
+
};
|
|
860
|
+
var query_string = _.HTTPBuildQuery(request_params);
|
|
861
|
+
var full_url = settings_endpoint + '?' + query_string;
|
|
862
|
+
var self = this;
|
|
863
|
+
|
|
864
|
+
var abortController = new AbortController();
|
|
865
|
+
var timeout_id = setTimeout(function() {
|
|
866
|
+
abortController.abort();
|
|
867
|
+
}, 500);
|
|
868
|
+
var fetchOptions = {
|
|
869
|
+
'method': 'GET',
|
|
870
|
+
'headers': {
|
|
871
|
+
'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
|
|
872
|
+
},
|
|
873
|
+
'signal': abortController.signal
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
return window['fetch'](full_url, fetchOptions).then(function(response) {
|
|
877
|
+
clearTimeout(timeout_id);
|
|
878
|
+
if (!response['ok']) {
|
|
879
|
+
console.critical('Network response was not ok');
|
|
880
|
+
disableRecordingIfStrict();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
return response.json();
|
|
884
|
+
}).then(function(result) {
|
|
885
|
+
if (result && result['sdk_config'] && result['sdk_config']['config']) {
|
|
886
|
+
var remote_config = result['sdk_config']['config'];
|
|
887
|
+
|
|
888
|
+
// Verify that remote config contains only valid keys from DEFAULT_CONFIG
|
|
889
|
+
var valid_config = {};
|
|
890
|
+
_.each(remote_config, function(value, key) {
|
|
891
|
+
if (DEFAULT_CONFIG.hasOwnProperty(key)) {
|
|
892
|
+
valid_config[key] = value;
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
if (_.isEmptyObject(valid_config)) {
|
|
897
|
+
console.critical('No valid config keys found in remote settings.');
|
|
898
|
+
disableRecordingIfStrict();
|
|
899
|
+
} else {
|
|
900
|
+
self.set_config(valid_config);
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
disableRecordingIfStrict();
|
|
904
|
+
}
|
|
905
|
+
}).catch(function(err) {
|
|
906
|
+
clearTimeout(timeout_id);
|
|
907
|
+
console.critical('Failed to fetch remote settings', err);
|
|
908
|
+
disableRecordingIfStrict();
|
|
909
|
+
});
|
|
910
|
+
};
|
|
911
|
+
|
|
824
912
|
/**
|
|
825
913
|
* _execute_array() deals with processing any mixpanel function
|
|
826
914
|
* calls that were called before the Mixpanel library were loaded
|
|
@@ -1149,6 +1237,11 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
|
|
|
1149
1237
|
send_request_options: options
|
|
1150
1238
|
}, callback);
|
|
1151
1239
|
|
|
1240
|
+
// Check for first-time event matches
|
|
1241
|
+
if (this.flags && this.flags.checkFirstTimeEvents) {
|
|
1242
|
+
this.flags.checkFirstTimeEvents(event_name, properties);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1152
1245
|
return ret;
|
|
1153
1246
|
});
|
|
1154
1247
|
|
package/src/recorder/index.js
CHANGED