mixpanel-browser 2.72.0 → 2.74.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 (44) hide show
  1. package/.claude/settings.local.json +5 -2
  2. package/.eslintrc.json +7 -4
  3. package/.github/workflows/integration-tests.yml +52 -0
  4. package/.github/workflows/unit-tests.yml +40 -0
  5. package/CHANGELOG.md +12 -0
  6. package/README.md +1 -1
  7. package/build.sh +1 -5
  8. package/dist/mixpanel-core.cjs.d.ts +49 -4
  9. package/dist/mixpanel-core.cjs.js +244 -26
  10. package/dist/mixpanel-recorder.js +5258 -688
  11. package/dist/mixpanel-recorder.min.js +1 -1
  12. package/dist/mixpanel-recorder.min.js.map +1 -1
  13. package/dist/mixpanel-with-async-recorder.cjs.d.ts +49 -4
  14. package/dist/mixpanel-with-async-recorder.cjs.js +244 -26
  15. package/dist/mixpanel-with-recorder.d.ts +49 -4
  16. package/dist/mixpanel-with-recorder.js +6858 -2099
  17. package/dist/mixpanel-with-recorder.min.d.ts +49 -4
  18. package/dist/mixpanel-with-recorder.min.js +1 -1
  19. package/dist/mixpanel.amd.d.ts +49 -4
  20. package/dist/mixpanel.amd.js +6858 -2099
  21. package/dist/mixpanel.cjs.d.ts +49 -4
  22. package/dist/mixpanel.cjs.js +6858 -2099
  23. package/dist/mixpanel.globals.js +244 -26
  24. package/dist/mixpanel.min.js +175 -171
  25. package/dist/mixpanel.module.d.ts +49 -4
  26. package/dist/mixpanel.module.js +6858 -2099
  27. package/dist/mixpanel.umd.d.ts +49 -4
  28. package/dist/mixpanel.umd.js +6858 -2099
  29. package/dist/rrweb-bundled.js +4315 -591
  30. package/dist/rrweb-compiled.js +4962 -641
  31. package/package.json +30 -5
  32. package/rollup.config.mjs +254 -224
  33. package/src/autocapture/utils.js +15 -7
  34. package/src/config.js +1 -1
  35. package/src/index.d.ts +49 -4
  36. package/src/mixpanel-core.js +215 -15
  37. package/src/recorder/masking.js +197 -0
  38. package/src/recorder/rrweb-entrypoint.js +2 -1
  39. package/src/recorder/session-recording.js +43 -4
  40. package/src/recorder/utils.js +5 -1
  41. package/src/utils.js +11 -2
  42. package/src/window.js +3 -1
  43. package/testServer.js +51 -7
  44. package/.github/workflows/tests.yml +0 -25
package/src/index.d.ts CHANGED
@@ -2,10 +2,12 @@ export type Persistence = "cookie" | "localStorage";
2
2
 
3
3
  export type ApiPayloadFormat = "base64" | "json";
4
4
 
5
- export type PushItem = Array<string | Dict>;
5
+ 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
  }
@@ -155,22 +157,32 @@ export interface FlagsConfig {
155
157
  context: Dict;
156
158
  }
157
159
 
160
+ export interface BeforeSendHookPayload {
161
+ event: string;
162
+ properties: Record<string, any>;
163
+ }
164
+
158
165
  export interface Config {
159
166
  api_host: string;
160
167
  api_routes: {
161
168
  track?: string;
162
169
  engage?: string;
163
170
  groups?: string;
171
+ record?: string;
172
+ flags?: string;
164
173
  };
165
174
  api_method: string;
166
175
  api_transport: string;
167
176
  app_host: string;
168
177
  api_payload_format: ApiPayloadFormat;
169
178
  autotrack: boolean;
179
+ batch_autostart: boolean;
180
+ batch_requests: boolean;
170
181
  cdn: string;
171
182
  cookie_domain: string;
172
183
  cross_site_cookie: boolean;
173
184
  cross_subdomain_cookie: boolean;
185
+ error_reporter: (msg: string, err?: Error) => void;
174
186
  flags: boolean | FlagsConfig;
175
187
  persistence: Persistence;
176
188
  persistence_name: string;
@@ -207,24 +219,54 @@ export interface Config {
207
219
  inapp_protocol: string;
208
220
  inapp_link_new_window: boolean;
209
221
  ignore_dnt: boolean;
210
- batch_requests: boolean;
211
222
  batch_size: number;
212
223
  batch_flush_interval_ms: number;
213
224
  batch_request_timeout_ms: number;
225
+ recorder_src: string;
214
226
  record_block_class: string | RegExp;
215
227
  record_block_selector: string;
216
228
  record_collect_fonts: boolean;
217
229
  record_idle_timeout_ms: number;
218
230
  record_inline_images: boolean;
219
231
  record_mask_text_class: string | RegExp;
220
- 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;
221
238
  record_min_ms: number;
222
239
  record_max_ms: number;
223
240
  record_sessions_percent: number;
224
241
  record_canvas: boolean;
225
242
  record_heatmap_data: boolean;
243
+ remote_settings_mode: RemoteSettingType;
244
+ hooks: {
245
+ before_identify?: (new_distinct_id: string) => string | null;
246
+ before_register?: (
247
+ props: Dict,
248
+ days_or_options?: number | Partial<RegisterOptions>
249
+ ) => Dict | Array<Dict | number | Partial<RegisterOptions>> | null;
250
+ before_register_once?: (
251
+ props: Dict,
252
+ default_value?: any,
253
+ days_or_options?: number | Partial<RegisterOptions>
254
+ ) => Dict | Array<any | Dict | number | Partial<RegisterOptions>> | null;
255
+ before_send_events?: (
256
+ event: BeforeSendHookPayload
257
+ ) => BeforeSendHookPayload | null;
258
+ before_track?: (
259
+ event_name: string,
260
+ properties: Dict
261
+ ) => string | Array<string | Dict> | null;
262
+ before_unregister?: (
263
+ property: string,
264
+ options?: Partial<RegisterOptions>
265
+ ) => string | Partial<RegisterOptions> | null;
266
+ };
226
267
  }
227
268
 
269
+
228
270
  export type VerboseResponse =
229
271
  | {
230
272
  status: 1;
@@ -323,10 +365,11 @@ export interface Mixpanel {
323
365
  get_distinct_id(): any;
324
366
  get_group(group_key: string, group_id: string): Group;
325
367
  get_property(property_name: string): any;
368
+ get_session_replay_url(): string;
326
369
  has_opted_in_tracking(options?: Partial<HasOptedInOutOptions>): boolean;
327
370
  has_opted_out_tracking(options?: Partial<HasOptedInOutOptions>): boolean;
328
371
  identify(unique_id?: string): any;
329
- init(token: string, config: Partial<Config>, name: string): Mixpanel;
372
+ init(token: string, config: Partial<Config>, name?: string): Mixpanel;
330
373
  opt_in_tracking(options?: Partial<InTrackingOptions>): void;
331
374
  opt_out_tracking(options?: Partial<OutTrackingOptions>): void;
332
375
  push(item: PushItem): void;
@@ -351,6 +394,7 @@ export interface Mixpanel {
351
394
  group_ids: string | string[] | number | number[],
352
395
  callback?: Callback
353
396
  ): void;
397
+ start_batch_senders(): void;
354
398
  time_event(event_name: string): void;
355
399
  track(
356
400
  event_name: string,
@@ -380,6 +424,7 @@ export interface Mixpanel {
380
424
  ): void;
381
425
  unregister(property: string, options?: Partial<RegisterOptions>): void;
382
426
  people: People;
427
+ start_batch_senders(): void;
383
428
  start_session_recording(): void;
384
429
  stop_session_recording(): void;
385
430
  get_session_recording_properties(): { $mp_replay_id?: string } | {};
@@ -57,12 +57,13 @@ var mixpanel_master; // main mixpanel instance / object
57
57
  var INIT_MODULE = 0;
58
58
  var INIT_SNIPPET = 1;
59
59
 
60
- var IDENTITY_FUNC = function(x) {return x;};
61
-
62
60
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
63
61
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
64
62
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
65
63
  /** @const */ var DEVICE_ID_PREFIX = '$device:';
64
+ /** @const */ var SETTING_STRICT = 'strict';
65
+ /** @const */ var SETTING_FALLBACK = 'fallback';
66
+ /** @const */ var SETTING_DISABLED = 'disabled';
66
67
 
67
68
 
68
69
  /*
@@ -91,7 +92,8 @@ var DEFAULT_API_ROUTES = {
91
92
  'engage': 'engage/',
92
93
  'groups': 'groups/',
93
94
  'record': 'record/',
94
- 'flags': 'flags/'
95
+ 'flags': 'flags/',
96
+ 'settings': 'settings/'
95
97
  };
96
98
 
97
99
  /*
@@ -155,12 +157,12 @@ var DEFAULT_CONFIG = {
155
157
  'record_console': true,
156
158
  'record_heatmap_data': false,
157
159
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
158
- 'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
159
- 'record_mask_text_selector': '*',
160
+ 'record_mask_inputs': true,
160
161
  'record_max_ms': MAX_RECORDING_MS,
161
162
  'record_min_ms': 0,
162
163
  'record_sessions_percent': 0,
163
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
164
+ 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
165
+ 'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
164
166
  };
165
167
 
166
168
  var DOM_LOADED = false;
@@ -224,6 +226,17 @@ var create_mplib = function(token, config, name) {
224
226
  // global debug to be true
225
227
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
226
228
 
229
+ var source = init_type === INIT_MODULE ? 'module' : 'snippet';
230
+ window.dispatchEvent(new window.CustomEvent('$mp_sdk_to_extension_event', {
231
+ 'detail': {
232
+ 'instance': instance,
233
+ 'source': source,
234
+ 'token': token,
235
+ 'name': name,
236
+ 'info': _.info
237
+ }
238
+ }));
239
+
227
240
  // if target is not defined, we called init after the lib already
228
241
  // loaded, so there won't be an array of things to execute
229
242
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -294,6 +307,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
294
307
  }
295
308
  }
296
309
 
310
+ this.hooks = {};
311
+
297
312
  this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
298
313
  'name': name,
299
314
  'token': token,
@@ -385,7 +400,16 @@ MixpanelLib.prototype._init = function(token, config, name) {
385
400
  this.autocapture.init();
386
401
 
387
402
  this._init_tab_id();
388
- this._check_and_start_session_recording();
403
+
404
+ // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
405
+ var mode = this.get_config('remote_settings_mode');
406
+ if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
407
+ this._fetch_remote_settings(mode).then(_.bind(function() {
408
+ this._check_and_start_session_recording();
409
+ }, this));
410
+ } else {
411
+ this._check_and_start_session_recording();
412
+ }
389
413
  };
390
414
 
391
415
  /**
@@ -810,6 +834,77 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
810
834
  return succeeded;
811
835
  };
812
836
 
837
+ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
838
+ var disableRecordingIfStrict = function() {
839
+ if (mode === 'strict') {
840
+ self.set_config({'record_sessions_percent': 0});
841
+ }
842
+ };
843
+
844
+ if (!window['AbortController']) {
845
+ console.critical('Remote settings unavailable: missing minimum required APIs');
846
+ disableRecordingIfStrict();
847
+ return Promise.resolve();
848
+ }
849
+
850
+ var settings_endpoint = this.get_api_host('settings') + '/' + this.get_config('api_routes')['settings'];
851
+ var request_params = {
852
+ '$lib_version': Config.LIB_VERSION,
853
+ 'mp_lib': 'web',
854
+ 'sdk_config': '1',
855
+ };
856
+ var query_string = _.HTTPBuildQuery(request_params);
857
+ var full_url = settings_endpoint + '?' + query_string;
858
+ var self = this;
859
+
860
+ var abortController = new AbortController();
861
+ var timeout_id = setTimeout(function() {
862
+ abortController.abort();
863
+ }, 500);
864
+ var fetchOptions = {
865
+ 'method': 'GET',
866
+ 'headers': {
867
+ 'Authorization': 'Basic ' + btoa(self.get_config('token') + ':'),
868
+ },
869
+ 'signal': abortController.signal
870
+ };
871
+
872
+ return window['fetch'](full_url, fetchOptions).then(function(response) {
873
+ clearTimeout(timeout_id);
874
+ if (!response['ok']) {
875
+ console.critical('Network response was not ok');
876
+ disableRecordingIfStrict();
877
+ return;
878
+ }
879
+ return response.json();
880
+ }).then(function(result) {
881
+ if (result && result['sdk_config'] && result['sdk_config']['config']) {
882
+ var remote_config = result['sdk_config']['config'];
883
+
884
+ // Verify that remote config contains only valid keys from DEFAULT_CONFIG
885
+ var valid_config = {};
886
+ _.each(remote_config, function(value, key) {
887
+ if (DEFAULT_CONFIG.hasOwnProperty(key)) {
888
+ valid_config[key] = value;
889
+ }
890
+ });
891
+
892
+ if (_.isEmptyObject(valid_config)) {
893
+ console.critical('No valid config keys found in remote settings.');
894
+ disableRecordingIfStrict();
895
+ } else {
896
+ self.set_config(valid_config);
897
+ }
898
+ } else {
899
+ disableRecordingIfStrict();
900
+ }
901
+ }).catch(function(err) {
902
+ clearTimeout(timeout_id);
903
+ console.critical('Failed to fetch remote settings', err);
904
+ disableRecordingIfStrict();
905
+ });
906
+ };
907
+
813
908
  /**
814
909
  * _execute_array() deals with processing any mixpanel function
815
910
  * calls that were called before the Mixpanel library were loaded
@@ -894,7 +989,12 @@ MixpanelLib.prototype.init_batchers = function() {
894
989
  );
895
990
  }, this),
896
991
  beforeSendHook: _.bind(function(item) {
897
- return this._run_hook('before_send_' + attrs.type, item);
992
+ var ret = this._run_hook('before_send_' + attrs.type, item);
993
+ if (ret) {
994
+ return ret[0];
995
+ } else {
996
+ return null;
997
+ }
898
998
  }, this),
899
999
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
900
1000
  usePersistence: true,
@@ -987,6 +1087,9 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
987
1087
  var send_request_immediately = _.bind(function() {
988
1088
  if (!send_request_options.skip_hooks) {
989
1089
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
1090
+ if (truncated_data) {
1091
+ truncated_data = truncated_data[0];
1092
+ }
990
1093
  }
991
1094
  if (truncated_data) {
992
1095
  console.log('MIXPANEL REQUEST:');
@@ -1041,6 +1144,17 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
1041
1144
  * with the tracking payload sent to the API server is returned; otherwise false.
1042
1145
  */
1043
1146
  MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) {
1147
+ var ret;
1148
+ if (!(options && options.skip_hooks)) {
1149
+ ret = this._run_hook('before_track', event_name, properties);
1150
+ if (ret === null) {
1151
+ return;
1152
+ } else {
1153
+ event_name = ret[0];
1154
+ properties = ret[1];
1155
+ }
1156
+ }
1157
+
1044
1158
  if (!callback && typeof options === 'function') {
1045
1159
  callback = options;
1046
1160
  options = null;
@@ -1110,7 +1224,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
1110
1224
  'event': event_name,
1111
1225
  'properties': properties
1112
1226
  };
1113
- var ret = this._track_or_batch({
1227
+ ret = this._track_or_batch({
1114
1228
  type: 'events',
1115
1229
  data: data,
1116
1230
  endpoint: this.get_api_host('events') + '/' + this.get_config('api_routes')['track'],
@@ -1456,6 +1570,14 @@ var options_for_register = function(days_or_options) {
1456
1570
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
1457
1571
  */
1458
1572
  MixpanelLib.prototype.register = function(props, days_or_options) {
1573
+ var ret = this._run_hook('before_register', props, days_or_options);
1574
+ if (ret === null) {
1575
+ return;
1576
+ } else {
1577
+ props = ret[0];
1578
+ days_or_options = ret[1];
1579
+ }
1580
+
1459
1581
  var options = options_for_register(days_or_options);
1460
1582
  if (options['persistent']) {
1461
1583
  this['persistence'].register(props, options['days']);
@@ -1492,6 +1614,15 @@ MixpanelLib.prototype.register = function(props, days_or_options) {
1492
1614
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
1493
1615
  */
1494
1616
  MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
1617
+ var ret = this._run_hook('before_register_once', props, default_value, days_or_options);
1618
+ if (ret === null) {
1619
+ return;
1620
+ } else {
1621
+ props = ret[0];
1622
+ default_value = ret[1];
1623
+ days_or_options = ret[2];
1624
+ }
1625
+
1495
1626
  var options = options_for_register(days_or_options);
1496
1627
  if (options['persistent']) {
1497
1628
  this['persistence'].register_once(props, default_value, options['days']);
@@ -1515,6 +1646,14 @@ MixpanelLib.prototype.register_once = function(props, default_value, days_or_opt
1515
1646
  * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
1516
1647
  */
1517
1648
  MixpanelLib.prototype.unregister = function(property, options) {
1649
+ var ret = this._run_hook('before_unregister', property, options);
1650
+ if (ret === null) {
1651
+ return;
1652
+ } else {
1653
+ property = ret[0];
1654
+ options = ret[1];
1655
+ }
1656
+
1518
1657
  options = options_for_register(options);
1519
1658
  if (options['persistent']) {
1520
1659
  this['persistence'].unregister(property);
@@ -1563,6 +1702,13 @@ MixpanelLib.prototype.identify = function(
1563
1702
  // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
1564
1703
  // _union_callback:function A callback to be run if and when the People union queue is flushed
1565
1704
  // _unset_callback:function A callback to be run if and when the People unset queue is flushed
1705
+ var ret = this._run_hook('before_identify', new_distinct_id);
1706
+
1707
+ if (ret === null) {
1708
+ return -1;
1709
+ } else {
1710
+ new_distinct_id = ret[0];
1711
+ }
1566
1712
 
1567
1713
  var previous_distinct_id = this.get_distinct_id();
1568
1714
  if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
@@ -1887,6 +2033,25 @@ MixpanelLib.prototype.set_config = function(config) {
1887
2033
  if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
1888
2034
  this.autocapture.init();
1889
2035
  }
2036
+
2037
+ if (_.isObject(config['hooks'])) {
2038
+ this.hooks = {};
2039
+ _.each(config['hooks'], function(hook_value, hook_name) {
2040
+ if (_.isFunction(hook_value)) {
2041
+ this.hooks[hook_name] = [hook_value];
2042
+ } else if (_.isArray(hook_value)) {
2043
+ this.hooks[hook_name] = [];
2044
+ for (var i = 0; i < hook_value.length; i++) {
2045
+ if (!_.isFunction(hook_value[i])) {
2046
+ console.critical('Invalid hook added. Hook is not a function');
2047
+ }
2048
+ this.hooks[hook_name].push(hook_value[i]);
2049
+ }
2050
+ } else {
2051
+ console.critical('Invalid hooks added. Ensure that the hook values passed into config.hooks are functions or arrays of functions.');
2052
+ }
2053
+ }, this);
2054
+ }
1890
2055
  }
1891
2056
  };
1892
2057
 
@@ -1904,12 +2069,26 @@ MixpanelLib.prototype.get_config = function(prop_name) {
1904
2069
  * @returns {any|null} return value of user-provided hook, or null if nothing was returned
1905
2070
  */
1906
2071
  MixpanelLib.prototype._run_hook = function(hook_name) {
1907
- var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
1908
- if (typeof ret === 'undefined') {
1909
- this.report_error(hook_name + ' hook did not return a value');
1910
- ret = null;
1911
- }
1912
- return ret;
2072
+ var hook_data = slice.call(arguments, 1);
2073
+ _.each(this.hooks[hook_name], function(hook) {
2074
+ if (hook_data === null) {
2075
+ return null;
2076
+ }
2077
+
2078
+ var ret = hook.apply(this, hook_data);
2079
+
2080
+ if (typeof ret === 'undefined') {
2081
+ this.report_error(hook_name + ' hook did not return a valid value');
2082
+ hook_data = null;
2083
+ } else {
2084
+ if (!_.isArray(ret)) {
2085
+ ret = [ret];
2086
+ }
2087
+ hook_data.splice.apply(hook_data, [0, ret.length].concat(ret));
2088
+ }
2089
+ }, this);
2090
+
2091
+ return hook_data;
1913
2092
  };
1914
2093
 
1915
2094
  /**
@@ -2220,6 +2399,25 @@ MixpanelLib.prototype.report_error = function(msg, err) {
2220
2399
  }
2221
2400
  };
2222
2401
 
2402
+ MixpanelLib.prototype.add_hook = function(hook_name, hook_fn) {
2403
+ if (!this.hooks[hook_name]) {
2404
+ this.hooks[hook_name] = [];
2405
+ }
2406
+ this.hooks[hook_name].push(hook_fn);
2407
+ };
2408
+
2409
+ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
2410
+ var fn_index;
2411
+ if (this.hooks[hook_name]) {
2412
+ fn_index = this.hooks[hook_name].indexOf(hook_fn);
2413
+ if (fn_index !== -1) {
2414
+ this.hooks[hook_name].splice(fn_index, 1);
2415
+ } else {
2416
+ console.log('remove_hook failed. Matching hook was not found');
2417
+ }
2418
+ }
2419
+ };
2420
+
2223
2421
  // EXPORTS (for closure compiler)
2224
2422
 
2225
2423
  // MixpanelLib Exports
@@ -2252,6 +2450,8 @@ MixpanelLib.prototype['get_group'] = MixpanelLib.protot
2252
2450
  MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group;
2253
2451
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
2254
2452
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
2453
+ MixpanelLib.prototype['add_hook'] = MixpanelLib.prototype.add_hook;
2454
+ MixpanelLib.prototype['remove_hook'] = MixpanelLib.prototype.remove_hook;
2255
2455
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
2256
2456
  MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
2257
2457
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @typedef {Object} MaskingConfig
3
+ * @property {string} maskingSelector - Combined CSS selector string to mask
4
+ * @property {string} unmaskingSelector - Combined CSS selector string to unmask
5
+ * @property {boolean} maskAll - Whether to mask all by default
6
+ * @property {RegExp} [_legacyClassRegex] - Legacy class regex for backwards compatibility
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} PrivacyConfig
11
+ * @property {MaskingConfig} input - Input masking configuration
12
+ * @property {MaskingConfig} text - Text masking configuration
13
+ */
14
+
15
+ import { classMatchesRegex } from './rrweb-entrypoint';
16
+ import { elementLooksSensitive } from '../autocapture/utils';
17
+
18
+ var COMMON_MASK_CLASSES_SELECTOR = '.mp-mask, .fs-mask, .amp-mask, .rr-mask, .ph-mask';
19
+ var RRWEB_PASSWORD_ATTRIBUTE = 'data-rr-is-password';
20
+ var ALWAYS_MASKED_INPUT_TYPES = ['password', 'email', 'tel', 'hidden'];
21
+
22
+ /**
23
+ * Normalizes a selector value to an array of selectors
24
+ * @param {string|string[]|undefined} selector
25
+ * @returns {string[]}
26
+ */
27
+ function normalizeSelectors(selector) {
28
+ if (!selector) {
29
+ return [];
30
+ }
31
+ if (Array.isArray(selector)) {
32
+ return selector;
33
+ }
34
+ return [selector];
35
+ }
36
+
37
+ /**
38
+ * Reads flat config options and normalizes them into internal privacy config structure
39
+ * @param {Object} mixpanelInstance
40
+ * @returns {PrivacyConfig} privacyConfig
41
+ */
42
+ function getPrivacyConfig(mixpanelInstance) {
43
+ var privacyConfig = {
44
+ input: {
45
+ maskingSelector: '',
46
+ unmaskingSelector: '',
47
+ maskAll: true
48
+ },
49
+ text: {
50
+ maskingSelector: '',
51
+ unmaskingSelector: '',
52
+ maskAll: true
53
+ }
54
+ };
55
+
56
+ // Input related config
57
+ var maskInputSelector = mixpanelInstance.get_config('record_mask_input_selector');
58
+ var unmaskInputSelector = mixpanelInstance.get_config('record_unmask_input_selector');
59
+ var maskAllInputs = mixpanelInstance.get_config('record_mask_all_inputs');
60
+
61
+ privacyConfig.input.maskingSelector = normalizeSelectors(maskInputSelector).join(',');
62
+ privacyConfig.input.unmaskingSelector = normalizeSelectors(unmaskInputSelector).join(',');
63
+ if (maskAllInputs !== undefined) {
64
+ privacyConfig.input.maskAll = maskAllInputs;
65
+ }
66
+
67
+ // Text related config
68
+ var maskTextSelector = mixpanelInstance.get_config('record_mask_text_selector');
69
+ var unmaskTextSelector = mixpanelInstance.get_config('record_unmask_text_selector');
70
+ var maskAllText = mixpanelInstance.get_config('record_mask_all_text');
71
+ var legacyMaskTextClass = mixpanelInstance.get_config('record_mask_text_class');
72
+
73
+ var textMaskingSelectors = normalizeSelectors(maskTextSelector);
74
+
75
+ // Handle legacy record_mask_text_class
76
+ if (legacyMaskTextClass) {
77
+ if (legacyMaskTextClass instanceof RegExp) {
78
+ // For RegExp classes, we'll need to handle this differently in the shouldMaskText function
79
+ privacyConfig.text._legacyClassRegex = legacyMaskTextClass;
80
+ } else {
81
+ // String class name - convert to selector
82
+ var classSelector = '.' + legacyMaskTextClass;
83
+ if (textMaskingSelectors.indexOf(classSelector) === -1) {
84
+ textMaskingSelectors.push(classSelector);
85
+ }
86
+ }
87
+ }
88
+
89
+ privacyConfig.text.maskingSelector = textMaskingSelectors.join(',');
90
+ privacyConfig.text.unmaskingSelector = normalizeSelectors(unmaskTextSelector).join(',');
91
+
92
+ // Migrate old config: if only record_mask_text_selector is specified, set maskAll to false
93
+ // preserves behavior where the masking selector defaulted to "*"
94
+ if (maskAllText === undefined && maskTextSelector !== undefined) {
95
+ privacyConfig.text.maskAll = false;
96
+ } else if (maskAllText !== undefined) {
97
+ privacyConfig.text.maskAll = maskAllText;
98
+ }
99
+
100
+ return privacyConfig;
101
+ }
102
+
103
+ /**
104
+ * Checks if element matches a combined CSS selector
105
+ * @param {HTMLElement} element
106
+ * @param {string} selector - Combined CSS selector (comma-separated)
107
+ * @returns {boolean}
108
+ */
109
+ function elementMatchesSelector(element, selector) {
110
+ if (!selector) {
111
+ return false;
112
+ }
113
+ return !!element.closest(selector);
114
+ }
115
+
116
+ /**
117
+ * Determines if an input should be masked based on privacy config
118
+ * @param {HTMLElement} element
119
+ * @param {PrivacyConfig} privacyConfig
120
+ * @returns {boolean}
121
+ */
122
+ function shouldMaskInput(element, privacyConfig) {
123
+ var inputType = (element.getAttribute('type') || '').toLowerCase();
124
+ if (ALWAYS_MASKED_INPUT_TYPES.indexOf(inputType) !== -1) {
125
+ return true;
126
+ }
127
+
128
+ var autocomplete = (element.getAttribute('autocomplete') || '').toLowerCase();
129
+ if (autocomplete && autocomplete !== '' && autocomplete !== 'off') {
130
+ return true;
131
+ }
132
+
133
+ if (element.hasAttribute(RRWEB_PASSWORD_ATTRIBUTE)) {
134
+ return true;
135
+ }
136
+
137
+ if (elementLooksSensitive(element)) {
138
+ return true;
139
+ }
140
+
141
+ if (privacyConfig.input.maskAll) {
142
+ if (elementMatchesSelector(element, privacyConfig.input.unmaskingSelector)) {
143
+ return false;
144
+ }
145
+
146
+ return true;
147
+ } else {
148
+ if (elementMatchesSelector(element, privacyConfig.input.maskingSelector)) {
149
+ return true;
150
+ }
151
+ if (elementMatchesSelector(element, COMMON_MASK_CLASSES_SELECTOR)) {
152
+ return true;
153
+ }
154
+ return false;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Determines if text should be masked based on privacy config
160
+ * @param {HTMLElement|null} element
161
+ * @param {PrivacyConfig} privacyConfig
162
+ * @returns {boolean}
163
+ */
164
+ function shouldMaskText(element, privacyConfig) {
165
+ if (!element) {
166
+ return false;
167
+ }
168
+
169
+ // Check legacy class regex if present (for backwards compatibility)
170
+ if (privacyConfig.text._legacyClassRegex) {
171
+ if (classMatchesRegex(element, privacyConfig.text._legacyClassRegex, true)) {
172
+ return true;
173
+ }
174
+ }
175
+
176
+ if (privacyConfig.text.maskAll) {
177
+ if (elementMatchesSelector(element, privacyConfig.text.unmaskingSelector)) {
178
+ return false;
179
+ }
180
+ return true;
181
+ } else {
182
+ if (elementMatchesSelector(element, privacyConfig.text.maskingSelector)) {
183
+ return true;
184
+ }
185
+ if (elementMatchesSelector(element, COMMON_MASK_CLASSES_SELECTOR)) {
186
+ return true;
187
+ }
188
+ return false;
189
+ }
190
+ }
191
+
192
+ export {
193
+ COMMON_MASK_CLASSES_SELECTOR,
194
+ getPrivacyConfig,
195
+ shouldMaskInput,
196
+ shouldMaskText
197
+ };