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.
- package/.claude/settings.local.json +5 -2
- 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 +1 -1
- package/build.sh +1 -5
- package/dist/mixpanel-core.cjs.d.ts +49 -4
- package/dist/mixpanel-core.cjs.js +244 -26
- package/dist/mixpanel-recorder.js +5258 -688
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +49 -4
- package/dist/mixpanel-with-async-recorder.cjs.js +244 -26
- package/dist/mixpanel-with-recorder.d.ts +49 -4
- package/dist/mixpanel-with-recorder.js +6858 -2099
- package/dist/mixpanel-with-recorder.min.d.ts +49 -4
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +49 -4
- package/dist/mixpanel.amd.js +6858 -2099
- package/dist/mixpanel.cjs.d.ts +49 -4
- package/dist/mixpanel.cjs.js +6858 -2099
- package/dist/mixpanel.globals.js +244 -26
- package/dist/mixpanel.min.js +175 -171
- package/dist/mixpanel.module.d.ts +49 -4
- package/dist/mixpanel.module.js +6858 -2099
- package/dist/mixpanel.umd.d.ts +49 -4
- package/dist/mixpanel.umd.js +6858 -2099
- package/dist/rrweb-bundled.js +4315 -591
- package/dist/rrweb-compiled.js +4962 -641
- package/package.json +30 -5
- package/rollup.config.mjs +254 -224
- package/src/autocapture/utils.js +15 -7
- package/src/config.js +1 -1
- package/src/index.d.ts +49 -4
- package/src/mixpanel-core.js +215 -15
- 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/utils.js +11 -2
- package/src/window.js +3 -1
- package/testServer.js +51 -7
- 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
|
|
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 } | {};
|
package/src/mixpanel-core.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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
|
+
};
|