mixpanel-browser 2.75.0 → 2.77.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 (62) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/dependabot.yml +8 -0
  3. package/.github/workflows/integration-tests.yml +4 -4
  4. package/.github/workflows/unit-tests.yml +4 -4
  5. package/CHANGELOG.md +14 -0
  6. package/build.sh +10 -8
  7. package/dist/async-modules/mixpanel-recorder-DLKbUIEE.js +23669 -0
  8. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js +2 -0
  9. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +1 -0
  10. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +2 -0
  11. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +1 -0
  12. package/dist/async-modules/mixpanel-targeting-CmVvUyFM.js +2520 -0
  13. package/dist/mixpanel-core.cjs.d.ts +70 -1
  14. package/dist/mixpanel-core.cjs.js +724 -426
  15. package/dist/mixpanel-recorder.js +791 -41
  16. package/dist/mixpanel-recorder.min.js +1 -1
  17. package/dist/mixpanel-recorder.min.js.map +1 -1
  18. package/dist/mixpanel-targeting.js +6 -62
  19. package/dist/mixpanel-targeting.min.js +1 -1
  20. package/dist/mixpanel-targeting.min.js.map +1 -1
  21. package/dist/mixpanel-with-async-modules.cjs.d.ts +70 -1
  22. package/dist/mixpanel-with-async-modules.cjs.js +724 -426
  23. package/dist/mixpanel-with-async-recorder.cjs.d.ts +70 -1
  24. package/dist/mixpanel-with-async-recorder.cjs.js +724 -426
  25. package/dist/mixpanel-with-recorder.d.ts +70 -1
  26. package/dist/mixpanel-with-recorder.js +1471 -450
  27. package/dist/mixpanel-with-recorder.min.d.ts +70 -1
  28. package/dist/mixpanel-with-recorder.min.js +1 -1
  29. package/dist/mixpanel.amd.d.ts +70 -1
  30. package/dist/mixpanel.amd.js +1473 -504
  31. package/dist/mixpanel.cjs.d.ts +70 -1
  32. package/dist/mixpanel.cjs.js +1473 -504
  33. package/dist/mixpanel.globals.js +724 -426
  34. package/dist/mixpanel.min.js +189 -182
  35. package/dist/mixpanel.module.d.ts +70 -1
  36. package/dist/mixpanel.module.js +1473 -504
  37. package/dist/mixpanel.umd.d.ts +70 -1
  38. package/dist/mixpanel.umd.js +1473 -504
  39. package/dist/rrweb-bundled.js +61 -9
  40. package/dist/rrweb-compiled.js +56 -9
  41. package/logo.svg +5 -0
  42. package/package.json +6 -4
  43. package/rollup.config.mjs +163 -46
  44. package/src/autocapture/index.js +10 -27
  45. package/src/config.js +9 -3
  46. package/src/flags/index.js +1 -2
  47. package/src/index.d.ts +70 -1
  48. package/src/mixpanel-core.js +77 -112
  49. package/src/recorder/index.js +1 -1
  50. package/src/recorder/recorder.js +5 -1
  51. package/src/recorder/rrweb-network-plugin.js +649 -0
  52. package/src/recorder/session-recording.js +36 -12
  53. package/src/recorder/utils.js +27 -1
  54. package/src/recorder-manager.js +324 -0
  55. package/src/request-batcher.js +1 -1
  56. package/src/targeting/event-matcher.js +2 -57
  57. package/src/targeting/index.js +1 -1
  58. package/src/targeting/loader.js +1 -1
  59. package/src/utils.js +13 -1
  60. package/testServer.js +69 -1
  61. package/src/globals.js +0 -14
  62. /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
package/src/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { RulesLogic } from 'json-logic-js';
2
+
1
3
  export type Persistence = "cookie" | "localStorage";
2
4
 
3
5
  export type ApiPayloadFormat = "base64" | "json";
@@ -8,6 +10,16 @@ export type Query = string | Element | Element[];
8
10
 
9
11
  export type RemoteSettingType = "disabled" | "fallback" | "strict";
10
12
 
13
+
14
+ export interface EventTriggerProps {
15
+ percentage: number;
16
+ property_filters?: RulesLogic;
17
+ }
18
+
19
+ export interface RecordingEventTriggers {
20
+ [eventName: string]: EventTriggerProps;
21
+ }
22
+
11
23
  export interface Dict {
12
24
  [key: string]: any;
13
25
  }
@@ -225,6 +237,9 @@ export interface Config {
225
237
  recorder_src: string;
226
238
  record_block_class: string | RegExp;
227
239
  record_block_selector: string;
240
+ record_console: boolean;
241
+ record_network: boolean;
242
+ record_network_options: NetworkRecordOptions;
228
243
  record_collect_fonts: boolean;
229
244
  record_idle_timeout_ms: number;
230
245
  record_inline_images: boolean;
@@ -237,10 +252,12 @@ export interface Config {
237
252
  record_mask_all_inputs: boolean;
238
253
  record_min_ms: number;
239
254
  record_max_ms: number;
240
- record_sessions_percent: number;
255
+ record_allowed_iframe_origins: string[];
241
256
  record_canvas: boolean;
257
+ recording_event_triggers: RecordingEventTriggers;
242
258
  record_heatmap_data: boolean;
243
259
  remote_settings_mode: RemoteSettingType;
260
+ record_sessions_percent: number;
244
261
  hooks: {
245
262
  before_identify?: (new_distinct_id: string) => string | null;
246
263
  before_register?: (
@@ -518,5 +535,57 @@ export function get_session_recording_properties():
518
535
  | { $mp_replay_id?: string }
519
536
  | {};
520
537
 
538
+ // Network Recording Plugin Types
539
+ export type InitiatorType =
540
+ | 'audio'
541
+ | 'beacon'
542
+ | 'body'
543
+ | 'css'
544
+ | 'early-hint'
545
+ | 'embed'
546
+ | 'fetch'
547
+ | 'frame'
548
+ | 'iframe'
549
+ | 'icon'
550
+ | 'image'
551
+ | 'img'
552
+ | 'input'
553
+ | 'link'
554
+ | 'navigation'
555
+ | 'object'
556
+ | 'ping'
557
+ | 'script'
558
+ | 'track'
559
+ | 'video'
560
+ | 'xmlhttprequest';
561
+
562
+ export interface NetworkRequest {
563
+ url: string;
564
+ method?: string;
565
+ initiatorType: InitiatorType;
566
+ status?: number;
567
+ startTime: number;
568
+ endTime: number;
569
+ timeOrigin: number;
570
+ requestHeaders?: Record<string, string>;
571
+ requestBody?: string;
572
+ responseHeaders?: Record<string, string>;
573
+ responseBody?: string;
574
+ }
575
+
576
+ export interface NetworkRecordOptions {
577
+ initiatorTypes?: InitiatorType[];
578
+ ignoreRequestUrls?: string[];
579
+ ignoreRequestFn?: (data: NetworkRequest) => boolean;
580
+ recordHeaders?: { request: string[]; response: string[] };
581
+ recordBodyUrls?: { request: string[]; response: string[] };
582
+ recordInitialRequests?: boolean;
583
+ }
584
+
585
+ export interface NetworkData {
586
+ requests: NetworkRequest[];
587
+ isInitial?: boolean;
588
+ }
589
+
521
590
  declare const mixpanel: OverridedMixpanel;
522
591
  export default mixpanel;
@@ -1,11 +1,10 @@
1
1
  /* eslint camelcase: "off" */
2
- import Config from './config';
2
+ import {Config, TARGETING_FILENAME} from './config';
3
3
  import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice, NOOP_FUNC, JSONStringify } from './utils';
4
- import { isRecordingExpired } from './recorder/utils';
5
4
  import { window } from './window';
6
- import { RECORDER_GLOBAL_NAME } from './globals';
7
5
  import { Autocapture } from './autocapture';
8
6
  import { FeatureFlagManager } from './flags';
7
+ import { RecorderManager } from './recorder-manager';
9
8
  import { FormTracker, LinkTracker } from './dom-trackers';
10
9
  import { RequestBatcher } from './request-batcher';
11
10
  import { MixpanelGroup } from './mixpanel-group';
@@ -23,7 +22,6 @@ import {
23
22
  clearOptInOut,
24
23
  addOptOutCheckMixpanelLib
25
24
  } from './gdpr-utils';
26
- import { IDBStorageWrapper, RECORDING_REGISTRY_STORE_NAME } from './storage/indexed-db';
27
25
 
28
26
  /*
29
27
  * Mixpanel JS Library
@@ -66,7 +64,6 @@ var INIT_SNIPPET = 1;
66
64
  /** @const */ var SETTING_FALLBACK = 'fallback';
67
65
  /** @const */ var SETTING_DISABLED = 'disabled';
68
66
 
69
-
70
67
  /*
71
68
  * Dynamic... constants? Is that an oxymoron?
72
69
  */
@@ -151,19 +148,24 @@ var DEFAULT_CONFIG = {
151
148
  'batch_request_timeout_ms': 90000,
152
149
  'batch_autostart': true,
153
150
  'hooks': {},
151
+ 'record_allowed_iframe_origins': [],
154
152
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
155
153
  'record_block_selector': 'img, video, audio',
156
154
  'record_canvas': false,
157
155
  'record_collect_fonts': false,
158
156
  'record_console': true,
159
157
  'record_heatmap_data': false,
158
+ 'recording_event_triggers': {},
160
159
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
161
160
  'record_mask_inputs': true,
162
161
  'record_max_ms': MAX_RECORDING_MS,
163
162
  'record_min_ms': 0,
163
+ 'record_network': false,
164
+ 'record_network_options': {},
164
165
  'record_sessions_percent': 0,
165
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
166
- 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
166
+ 'recorder_src': null,
167
+ 'targeting_src': null,
168
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
167
169
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
168
170
  };
169
171
 
@@ -317,6 +319,19 @@ MixpanelLib.prototype._init = function(token, config, name) {
317
319
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
318
320
  }));
319
321
 
322
+ this.recorderManager = new RecorderManager({
323
+ mixpanelInstance: this,
324
+ getConfigFunc: _.bind(this.get_config, this),
325
+ setConfigFunc: _.bind(this.set_config, this),
326
+ getTabIdFunc: _.bind(this.get_tab_id, this),
327
+ reportErrorFunc: _.bind(this.report_error, this),
328
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
329
+ recorderSrc: this.get_config('recorder_src'),
330
+ targetingSrc: this.get_config('targeting_src'),
331
+ libBasePath: this.get_config('lib_base_path'),
332
+ loadExtraBundle: load_extra_bundle
333
+ });
334
+
320
335
  this['_jsc'] = NOOP_FUNC;
321
336
 
322
337
  this.__dom_loaded_queue = [];
@@ -395,7 +410,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
395
410
  getPropertyFunc: _.bind(this.get_property, this),
396
411
  trackingFunc: _.bind(this.track, this),
397
412
  loadExtraBundle: load_extra_bundle,
398
- targetingSrc: this.get_config('targeting_src')
413
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
399
414
  });
400
415
  this.flags.init();
401
416
  this['flags'] = this.flags;
@@ -408,11 +423,11 @@ MixpanelLib.prototype._init = function(token, config, name) {
408
423
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
409
424
  var mode = this.get_config('remote_settings_mode');
410
425
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
411
- this._fetch_remote_settings(mode).then(_.bind(function() {
412
- this._check_and_start_session_recording();
426
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
427
+ return this._check_and_start_session_recording();
413
428
  }, this));
414
429
  } else {
415
- this._check_and_start_session_recording();
430
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
416
431
  }
417
432
  };
418
433
 
@@ -456,132 +471,50 @@ MixpanelLib.prototype.get_tab_id = function () {
456
471
  return this.tab_id || null;
457
472
  };
458
473
 
459
- MixpanelLib.prototype._should_load_recorder = function () {
460
- if (this.get_config('disable_persistence')) {
461
- console.log('Load recorder check skipped due to disable_persistence config');
462
- return Promise.resolve(false);
463
- }
464
-
465
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
466
- var tab_id = this.get_tab_id();
467
- return recording_registry_idb.init()
468
- .then(function () {
469
- return recording_registry_idb.getAll();
470
- })
471
- .then(function (recordings) {
472
- for (var i = 0; i < recordings.length; i++) {
473
- // if there are expired recordings in the registry, we should load the recorder to flush them
474
- // if there's a recording for this tab id, we should load the recorder to continue the recording
475
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
476
- return true;
477
- }
478
- }
479
- return false;
480
- })
481
- .catch(_.bind(function (err) {
482
- this.report_error('Error checking recording registry', err);
483
- }, this));
484
- };
485
-
486
474
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
487
- if (!window['MutationObserver']) {
488
- console.critical('Browser does not support MutationObserver; skipping session recording');
489
- return;
490
- }
491
-
492
- var loadRecorder = _.bind(function(startNewIfInactive) {
493
- var handleLoadedRecorder = _.bind(function() {
494
- this._recorder = this._recorder || new window[RECORDER_GLOBAL_NAME](this);
495
- this._recorder['resumeRecording'](startNewIfInactive);
496
- }, this);
497
-
498
- if (_.isUndefined(window[RECORDER_GLOBAL_NAME])) {
499
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
500
- } else {
501
- handleLoadedRecorder();
502
- }
503
- }, this);
504
-
505
- /**
506
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
507
- * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
508
- */
509
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
510
- if (force_start || is_sampled) {
511
- loadRecorder(true);
512
- } else {
513
- this._should_load_recorder()
514
- .then(function (shouldLoad) {
515
- if (shouldLoad) {
516
- loadRecorder(false);
517
- }
518
- });
519
- }
475
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
520
476
  });
521
477
 
478
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
479
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
480
+ };
481
+
522
482
  MixpanelLib.prototype.start_session_recording = function () {
523
- this._check_and_start_session_recording(true);
483
+ return this._check_and_start_session_recording(true);
524
484
  };
525
485
 
526
486
  MixpanelLib.prototype.stop_session_recording = function () {
527
- if (this._recorder) {
528
- return this._recorder['stopRecording']();
529
- }
530
- return Promise.resolve();
487
+ return this.recorderManager.stopSessionRecording();
531
488
  };
532
489
 
533
490
  MixpanelLib.prototype.pause_session_recording = function () {
534
- if (this._recorder) {
535
- return this._recorder['pauseRecording']();
536
- }
537
- return Promise.resolve();
491
+ return this.recorderManager.pauseSessionRecording();
538
492
  };
539
493
 
540
494
  MixpanelLib.prototype.resume_session_recording = function () {
541
- if (this._recorder) {
542
- return this._recorder['resumeRecording']();
543
- }
544
- return Promise.resolve();
495
+ return this.recorderManager.resumeSessionRecording();
545
496
  };
546
497
 
547
498
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
548
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
499
+ return this.recorderManager.isRecordingHeatmapData();
549
500
  };
550
501
 
551
502
  MixpanelLib.prototype.get_session_recording_properties = function () {
552
- var props = {};
553
- var replay_id = this._get_session_replay_id();
554
- if (replay_id) {
555
- props['$mp_replay_id'] = replay_id;
556
- }
557
- return props;
503
+ return this.recorderManager.getSessionRecordingProperties();
558
504
  };
559
505
 
560
506
  MixpanelLib.prototype.get_session_replay_url = function () {
561
- var replay_url = null;
562
- var replay_id = this._get_session_replay_id();
563
- if (replay_id) {
564
- var query_params = _.HTTPBuildQuery({
565
- 'replay_id': replay_id,
566
- 'distinct_id': this.get_distinct_id(),
567
- 'token': this.get_config('token')
568
- });
569
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
570
- }
571
- return replay_url;
572
- };
573
-
574
- MixpanelLib.prototype._get_session_replay_id = function () {
575
- var replay_id = null;
576
- if (this._recorder) {
577
- replay_id = this._recorder['replayId'];
578
- }
579
- return replay_id || null;
507
+ return this.recorderManager.getSessionReplayUrl();
580
508
  };
581
509
 
582
510
  // "private" public method to reach into the recorder in test cases
583
511
  MixpanelLib.prototype.__get_recorder = function () {
584
- return this._recorder;
512
+ return this.recorderManager.getRecorder();
513
+ };
514
+
515
+ // "private" public method to get session recording init promise in test cases
516
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
517
+ return this.__session_recording_init_promise;
585
518
  };
586
519
 
587
520
  // Private methods
@@ -839,6 +772,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
839
772
  };
840
773
 
841
774
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
775
+ var self = this;
842
776
  var disableRecordingIfStrict = function() {
843
777
  if (mode === 'strict') {
844
778
  self.set_config({'record_sessions_percent': 0});
@@ -859,7 +793,6 @@ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
859
793
  };
860
794
  var query_string = _.HTTPBuildQuery(request_params);
861
795
  var full_url = settings_endpoint + '?' + query_string;
862
- var self = this;
863
796
 
864
797
  var abortController = new AbortController();
865
798
  var timeout_id = setTimeout(function() {
@@ -1051,6 +984,34 @@ MixpanelLib.prototype.push = function(item) {
1051
984
  this._execute_array([item]);
1052
985
  };
1053
986
 
987
+ /**
988
+ * Enables events on the Mixpanel object. If passed no arguments,
989
+ * this function enable tracking of all events. If passed an
990
+ * array of event names, those events will be enabled, but other
991
+ * existing disabled events will continue to be not tracked.
992
+ *
993
+ * @param {Array} [events] An array of event names to enable
994
+ */
995
+ MixpanelLib.prototype.enable = function(events) {
996
+ var keys, new_disabled_events, i, j;
997
+
998
+ if (typeof(events) === 'undefined') {
999
+ this._flags.disable_all_events = false;
1000
+ } else {
1001
+ keys = {};
1002
+ new_disabled_events = [];
1003
+ for (i = 0; i < events.length; i++) {
1004
+ keys[events[i]] = true;
1005
+ }
1006
+ for (j = 0; j < this.__disabled_events.length; j++) {
1007
+ if (!keys[this.__disabled_events[j]]) {
1008
+ new_disabled_events.push(this.__disabled_events[j]);
1009
+ }
1010
+ }
1011
+ this.__disabled_events = new_disabled_events;
1012
+ }
1013
+ };
1014
+
1054
1015
  /**
1055
1016
  * Disable events on the Mixpanel object. If passed no arguments,
1056
1017
  * this function disables tracking of any event. If passed an
@@ -1224,6 +1185,8 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
1224
1185
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
1225
1186
  }
1226
1187
 
1188
+ this._start_recording_on_event(event_name, properties);
1189
+
1227
1190
  var data = {
1228
1191
  'event': event_name,
1229
1192
  'properties': properties
@@ -2432,6 +2395,7 @@ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
2432
2395
  // MixpanelLib Exports
2433
2396
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
2434
2397
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
2398
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
2435
2399
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
2436
2400
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
2437
2401
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -2475,6 +2439,7 @@ MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES
2475
2439
 
2476
2440
  // Exports intended only for testing
2477
2441
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
2442
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
2478
2443
 
2479
2444
  // MixpanelPersistence Exports
2480
2445
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
@@ -1,5 +1,5 @@
1
1
  import { window } from '../window';
2
- import { RECORDER_GLOBAL_NAME } from '../globals';
2
+ import { RECORDER_GLOBAL_NAME } from '../config';
3
3
  import { MixpanelRecorder } from './recorder';
4
4
 
5
5
  window[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
@@ -125,8 +125,12 @@ MixpanelRecorder.prototype.resetRecording = function () {
125
125
  this.startRecording({shouldStopBatcher: true});
126
126
  };
127
127
 
128
+ MixpanelRecorder.prototype.isRecording = function () {
129
+ return this.activeRecording && !this.activeRecording.isRrwebStopped();
130
+ };
131
+
128
132
  MixpanelRecorder.prototype.getActiveReplayId = function () {
129
- if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
133
+ if (this.isRecording()) {
130
134
  return this.activeRecording.replayId;
131
135
  } else {
132
136
  return null;