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.
Files changed (55) hide show
  1. package/.eslintrc.json +7 -4
  2. package/.github/workflows/integration-tests.yml +52 -0
  3. package/.github/workflows/unit-tests.yml +40 -0
  4. package/CHANGELOG.md +12 -0
  5. package/README.md +3 -3
  6. package/build.sh +1 -5
  7. package/dist/mixpanel-core.cjs.d.ts +12 -1
  8. package/dist/mixpanel-core.cjs.js +432 -34
  9. package/dist/mixpanel-recorder.js +5364 -684
  10. package/dist/mixpanel-recorder.min.js +1 -1
  11. package/dist/mixpanel-recorder.min.js.map +1 -1
  12. package/dist/mixpanel-targeting.js +2576 -0
  13. package/dist/mixpanel-targeting.min.js +2 -0
  14. package/dist/mixpanel-targeting.min.js.map +1 -0
  15. package/dist/mixpanel-with-async-modules.cjs.d.ts +522 -0
  16. package/dist/mixpanel-with-async-modules.cjs.js +9700 -0
  17. package/dist/mixpanel-with-async-recorder.cjs.d.ts +12 -1
  18. package/dist/mixpanel-with-async-recorder.cjs.js +432 -34
  19. package/dist/mixpanel-with-recorder.d.ts +12 -1
  20. package/dist/mixpanel-with-recorder.js +7889 -2839
  21. package/dist/mixpanel-with-recorder.min.d.ts +12 -1
  22. package/dist/mixpanel-with-recorder.min.js +1 -1
  23. package/dist/mixpanel.amd.d.ts +12 -1
  24. package/dist/mixpanel.amd.js +8446 -2813
  25. package/dist/mixpanel.cjs.d.ts +12 -1
  26. package/dist/mixpanel.cjs.js +8446 -2813
  27. package/dist/mixpanel.globals.js +432 -34
  28. package/dist/mixpanel.min.js +182 -173
  29. package/dist/mixpanel.module.d.ts +12 -1
  30. package/dist/mixpanel.module.js +8446 -2813
  31. package/dist/mixpanel.umd.d.ts +12 -1
  32. package/dist/mixpanel.umd.js +8446 -2813
  33. package/dist/rrweb-bundled.js +4434 -596
  34. package/dist/rrweb-compiled.js +5078 -646
  35. package/package.json +33 -7
  36. package/rollup.config.mjs +286 -224
  37. package/src/autocapture/utils.js +15 -7
  38. package/src/config.js +1 -1
  39. package/src/flags/index.js +269 -8
  40. package/src/globals.js +14 -0
  41. package/src/index.d.ts +12 -1
  42. package/src/loaders/loader-module.js +1 -0
  43. package/src/mixpanel-core.js +101 -8
  44. package/src/recorder/index.js +2 -1
  45. package/src/recorder/masking.js +197 -0
  46. package/src/recorder/rrweb-entrypoint.js +2 -1
  47. package/src/recorder/session-recording.js +43 -4
  48. package/src/recorder/utils.js +5 -1
  49. package/src/targeting/event-matcher.js +97 -0
  50. package/src/targeting/index.js +11 -0
  51. package/src/targeting/loader.js +36 -0
  52. package/src/utils.js +12 -10
  53. package/testServer.js +51 -7
  54. package/.github/workflows/tests.yml +0 -25
  55. /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
@@ -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
- // filter out data from fields that look like sensitive fields
561
- var name = el.name || el.id || '';
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
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.73.0'
3
+ LIB_VERSION: '2.75.0'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -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
+ };
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;
@@ -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';
@@ -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
- 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
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
- this._check_and_start_session_recording();
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['__mp_recorder'](this);
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['__mp_recorder'])) {
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
 
@@ -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;