mixpanel-browser 2.71.1 → 2.73.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 (40) hide show
  1. package/.github/workflows/tests.yml +1 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/mixpanel-core.cjs.d.ts +84 -13
  4. package/dist/mixpanel-core.cjs.js +180 -28
  5. package/dist/mixpanel-recorder.js +684 -114
  6. package/dist/mixpanel-recorder.min.js +1 -1
  7. package/dist/mixpanel-recorder.min.js.map +1 -1
  8. package/dist/mixpanel-with-async-recorder.cjs.d.ts +84 -13
  9. package/dist/mixpanel-with-async-recorder.cjs.js +180 -28
  10. package/dist/mixpanel-with-recorder.d.ts +84 -13
  11. package/dist/mixpanel-with-recorder.js +860 -140
  12. package/dist/mixpanel-with-recorder.min.d.ts +84 -13
  13. package/dist/mixpanel-with-recorder.min.js +1 -1
  14. package/dist/mixpanel.amd.d.ts +84 -13
  15. package/dist/mixpanel.amd.js +860 -140
  16. package/dist/mixpanel.cjs.d.ts +84 -13
  17. package/dist/mixpanel.cjs.js +860 -140
  18. package/dist/mixpanel.globals.js +180 -28
  19. package/dist/mixpanel.min.js +172 -170
  20. package/dist/mixpanel.module.d.ts +84 -13
  21. package/dist/mixpanel.module.js +860 -140
  22. package/dist/mixpanel.umd.d.ts +84 -13
  23. package/dist/mixpanel.umd.js +860 -140
  24. package/dist/rrweb-bundled.js +12760 -0
  25. package/dist/rrweb-compiled.js +2496 -7176
  26. package/package.json +3 -2
  27. package/rollup.config.mjs +15 -4
  28. package/src/autocapture/index.js +1 -1
  29. package/src/autocapture/rageclick.js +20 -1
  30. package/src/autocapture/shadow-dom-observer.js +3 -15
  31. package/src/autocapture/utils.js +30 -0
  32. package/src/config.js +1 -1
  33. package/src/index.d.ts +84 -13
  34. package/src/mixpanel-core.js +127 -10
  35. package/src/recorder/recorder.js +1 -1
  36. package/src/recorder/rrweb-entrypoint.js +6 -0
  37. package/src/recorder/session-recording.js +69 -12
  38. package/src/utils.js +24 -0
  39. package/src/window.js +3 -1
  40. package/.claude/settings.local.json +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.71.1",
3
+ "version": "2.73.0",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
6
  "module": "dist/mixpanel.module.js",
@@ -66,6 +66,7 @@
66
66
  "webpack": "1.12.2"
67
67
  },
68
68
  "dependencies": {
69
- "@mixpanel/rrweb": "2.0.0-alpha.18.2"
69
+ "@mixpanel/rrweb": "2.0.0-alpha.18.2",
70
+ "@mixpanel/rrweb-plugin-console-record": "2.0.0-alpha.18.2"
70
71
  }
71
72
  }
package/rollup.config.mjs CHANGED
@@ -7,10 +7,11 @@ import fs from 'fs';
7
7
  import path from 'path';
8
8
 
9
9
  const COMPILED_RRWEB_PATH = 'build/rrweb-compiled.js';
10
+ const BUNDLED_RRWEB_PATH = 'build/rrweb-bundled.js';
10
11
 
11
12
  const aliasRrweb = () => alias({
12
13
  entries: [
13
- { find: '@mixpanel/rrweb', replacement: COMPILED_RRWEB_PATH },
14
+ { find: /rrweb-entrypoint(?:\.js)?$/, replacement: COMPILED_RRWEB_PATH },
14
15
  ]
15
16
  });
16
17
 
@@ -42,15 +43,25 @@ const MINIFY = process.env.MINIFY || process.env.FULL;
42
43
 
43
44
  // Main builds used to develop / iterate quickly
44
45
  const MAIN_BUILDS = [
45
- // compile rrweb first to es5 with swc, we'll replace the import later on
46
46
  {
47
- 'input': '@mixpanel/rrweb',
47
+ 'input': 'src/recorder/rrweb-entrypoint.js',
48
+ 'output': [
49
+ {
50
+ file: BUNDLED_RRWEB_PATH,
51
+ format: 'es',
52
+ }
53
+ ],
54
+ plugins: [nodeResolve({browser: true})]
55
+ },
56
+ {
57
+ 'input': BUNDLED_RRWEB_PATH,
48
58
  'output': [
49
59
  {
50
60
  file: COMPILED_RRWEB_PATH,
61
+ format: 'es',
51
62
  }
52
63
  ],
53
- plugins: [nodeResolve({browser: true}), swc({swc: {jsc: {target: 'es5'}}})]
64
+ plugins: [swc({swc: {jsc: {target: 'es5'}}})]
54
65
  },
55
66
 
56
67
  // IIFE recorder bundle that is loaded asynchronously
@@ -449,7 +449,7 @@ Autocapture.prototype.initRageClickTracking = function() {
449
449
  return;
450
450
  }
451
451
 
452
- if (this._rageClickTracker.isRageClick(ev['pageX'], ev['pageY'], currentRageClickConfig)) {
452
+ if (this._rageClickTracker.isRageClick(ev, currentRageClickConfig)) {
453
453
  this.trackDomEvent(ev, MP_EV_RAGE_CLICK);
454
454
  }
455
455
  }.bind(this);
@@ -1,17 +1,36 @@
1
+ import { getClickEventTargetElement, isDefinitelyNonInteractive } from './utils';
2
+
1
3
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
2
4
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
3
5
  /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
6
+ /** @const */ var DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY = false;
4
7
 
5
8
  function RageClickTracker() {
6
9
  this.clicks = [];
7
10
  }
8
11
 
9
- RageClickTracker.prototype.isRageClick = function(x, y, options) {
12
+ /**
13
+ * Determines if a click event is part of a rage click sequence.
14
+ * @param {Event} event - the original click event.
15
+ * @param {import('../index.d.ts').RageClickConfig} options - configuration options for rage click detection.
16
+ * @returns {boolean} - true if the click is considered a rage click, false otherwise.
17
+ */
18
+ RageClickTracker.prototype.isRageClick = function(event, options) {
10
19
  options = options || {};
11
20
  var thresholdPx = options['threshold_px'] || DEFAULT_RAGE_CLICK_THRESHOLD_PX;
12
21
  var timeoutMs = options['timeout_ms'] || DEFAULT_RAGE_CLICK_TIMEOUT_MS;
13
22
  var clickCount = options['click_count'] || DEFAULT_RAGE_CLICK_CLICK_COUNT;
23
+ var interactiveElementsOnly = options['interactive_elements_only'] || DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY;
24
+
25
+ if (interactiveElementsOnly) {
26
+ var target = getClickEventTargetElement(event);
27
+ if (!target || isDefinitelyNonInteractive(target)) {
28
+ return false;
29
+ }
30
+ }
31
+
14
32
  var timestamp = Date.now();
33
+ var x = event['pageX'], y = event['pageY'];
15
34
 
16
35
  var lastClick = this.clicks[this.clicks.length - 1];
17
36
  if (
@@ -1,4 +1,4 @@
1
- import { logger, weakSetSupported } from './utils';
1
+ import { getClickEventComposedPath, getClickEventTargetElement, logger, weakSetSupported } from './utils';
2
2
 
3
3
  function ShadowDOMObserver(changeCallback, observerConfig) {
4
4
  this.changeCallback = changeCallback || function() {};
@@ -12,28 +12,16 @@ ShadowDOMObserver.prototype.getEventTarget = function(event) {
12
12
  if (!this.observedShadowRoots) {
13
13
  return;
14
14
  }
15
- var path = this.getComposedPath(event);
16
- if (path && path.length) {
17
- return path[0];
18
- }
19
15
 
20
- return event['target'] || event['srcElement'];
16
+ return getClickEventTargetElement(event);
21
17
  };
22
18
 
23
-
24
- ShadowDOMObserver.prototype.getComposedPath = function(event) {
25
- if ('composedPath' in event) {
26
- return event['composedPath']();
27
- }
28
-
29
- return [];
30
- };
31
19
  ShadowDOMObserver.prototype.observeFromEvent = function(event) {
32
20
  if (!this.observedShadowRoots) {
33
21
  return;
34
22
  }
35
23
 
36
- var path = this.getComposedPath(event);
24
+ var path = getClickEventComposedPath(event);
37
25
 
38
26
  // Check each element in path for shadow roots
39
27
  for (var i = 0; i < path.length; i++) {
@@ -744,9 +744,39 @@ function isDefinitelyNonInteractive(element) {
744
744
  return false;
745
745
  }
746
746
 
747
+ /**
748
+ * Get the composed path of a click event for elements embedded in shadow DOM.
749
+ * @param {Event} event - event to get the composed path from
750
+ * @returns {Array} the composed path of the click event
751
+ */
752
+ function getClickEventComposedPath(event) {
753
+ if ('composedPath' in event) {
754
+ return event['composedPath']();
755
+ }
756
+
757
+ return [];
758
+ }
759
+
760
+ /**
761
+ * Get the element from a click event, accounting for elements embedded in shadow DOM.
762
+ * @param {Event} event - event to get the target from
763
+ * @returns {Element | null} the element that was the target of the click event
764
+ */
765
+ function getClickEventTargetElement(event) {
766
+ var path = getClickEventComposedPath(event);
767
+
768
+ if (path && path.length > 0) {
769
+ return path[0];
770
+ }
771
+
772
+ return event['target'] || event['srcElement'];
773
+ }
774
+
747
775
  export {
748
776
  EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_INPUT, EV_LOAD,EV_MP_LOCATION_CHANGE, EV_POPSTATE,
749
777
  EV_SCROLL, EV_SCROLLEND, EV_SELECT, EV_SUBMIT, EV_TOGGLE, EV_VISIBILITYCHANGE,
778
+ getClickEventComposedPath,
779
+ getClickEventTargetElement,
750
780
  getPolyfillScrollEndFunction,
751
781
  getPropsForDOMEvent,
752
782
  getSafeText,
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.71.1'
3
+ LIB_VERSION: '2.73.0'
4
4
  };
5
5
 
6
6
  export default Config;
package/src/index.d.ts CHANGED
@@ -2,7 +2,7 @@ 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
 
@@ -43,20 +43,22 @@ export interface OutTrackingOptions extends ClearOptOutInOutOptions {
43
43
  export type RageClickConfig =
44
44
  | boolean
45
45
  | {
46
- /** Distance threshold in pixels for clicks to be considered within the same area (default: 30) */
47
- threshold_px?: number;
48
- /** Time window in milliseconds for clicks to be considered rapid (default: 1000) */
49
- timeout_ms?: number;
50
- /** Number of clicks required to trigger a rage click event (default: 3) */
51
- click_count?: number;
52
- };
46
+ /** Distance threshold in pixels for clicks to be considered within the same area (default: 30) */
47
+ threshold_px?: number;
48
+ /** Time window in milliseconds for clicks to be considered rapid (default: 1000) */
49
+ timeout_ms?: number;
50
+ /** Number of clicks required to trigger a rage click event (default: 3) */
51
+ click_count?: number;
52
+ /** Whether to only track rage clicks on interactive elements like buttons, links, inputs (default: false) */
53
+ interactive_elements_only?: boolean;
54
+ };
53
55
 
54
56
  export type DeadClickConfig =
55
57
  | boolean
56
58
  | {
57
- /** Time in milliseconds to wait after a click before qualifying it as dead (default: 500) */
58
- timeout_ms?: number;
59
- };
59
+ /** Time in milliseconds to wait after a click before qualifying it as dead (default: 500) */
60
+ timeout_ms?: number;
61
+ };
60
62
 
61
63
  export interface RegisterOptions {
62
64
  persistent: boolean;
@@ -149,6 +151,15 @@ export interface AutocaptureConfig {
149
151
  block_element_callback?: (element: Element, event: Event) => boolean;
150
152
  }
151
153
 
154
+ export interface FlagsConfig {
155
+ context: Dict;
156
+ }
157
+
158
+ export interface BeforeSendHookPayload {
159
+ event: string;
160
+ properties: Record<string, any>;
161
+ }
162
+
152
163
  export interface Config {
153
164
  api_host: string;
154
165
  api_routes: {
@@ -161,10 +172,14 @@ export interface Config {
161
172
  app_host: string;
162
173
  api_payload_format: ApiPayloadFormat;
163
174
  autotrack: boolean;
175
+ batch_autostart: boolean;
176
+ batch_requests: boolean;
164
177
  cdn: string;
165
178
  cookie_domain: string;
166
179
  cross_site_cookie: boolean;
167
180
  cross_subdomain_cookie: boolean;
181
+ error_reporter: (msg: string, err?: Error) => void;
182
+ flags: boolean | FlagsConfig;
168
183
  persistence: Persistence;
169
184
  persistence_name: string;
170
185
  cookie_name: string;
@@ -200,10 +215,10 @@ export interface Config {
200
215
  inapp_protocol: string;
201
216
  inapp_link_new_window: boolean;
202
217
  ignore_dnt: boolean;
203
- batch_requests: boolean;
204
218
  batch_size: number;
205
219
  batch_flush_interval_ms: number;
206
220
  batch_request_timeout_ms: number;
221
+ recorder_src: string;
207
222
  record_block_class: string | RegExp;
208
223
  record_block_selector: string;
209
224
  record_collect_fonts: boolean;
@@ -216,6 +231,29 @@ export interface Config {
216
231
  record_sessions_percent: number;
217
232
  record_canvas: boolean;
218
233
  record_heatmap_data: boolean;
234
+ hooks: {
235
+ before_identify?: (new_distinct_id: string) => string | null;
236
+ before_register?: (
237
+ props: Dict,
238
+ days_or_options?: number | Partial<RegisterOptions>
239
+ ) => Dict | Array<Dict | number | Partial<RegisterOptions>> | null;
240
+ before_register_once?: (
241
+ props: Dict,
242
+ default_value?: any,
243
+ days_or_options?: number | Partial<RegisterOptions>
244
+ ) => Dict | Array<any | Dict | number | Partial<RegisterOptions>> | null;
245
+ before_send_events?: (
246
+ event: BeforeSendHookPayload
247
+ ) => BeforeSendHookPayload | null;
248
+ before_track?: (
249
+ event_name: string,
250
+ properties: Dict
251
+ ) => string | Array<string | Dict> | null;
252
+ before_unregister?: (
253
+ property: string,
254
+ options?: Partial<RegisterOptions>
255
+ ) => string | Partial<RegisterOptions> | null;
256
+ };
219
257
  }
220
258
 
221
259
  export type VerboseResponse =
@@ -277,19 +315,50 @@ export interface Group {
277
315
  unset(prop: string, callback?: Callback): void;
278
316
  }
279
317
 
318
+ export interface FlagsVariant {
319
+ key: string;
320
+ value: any;
321
+ experiment_id?: string;
322
+ is_experiment_active?: boolean;
323
+ is_qa_tester?: boolean;
324
+ }
325
+
326
+ export interface FlagsUpdateContextOptions {
327
+ replace?: boolean;
328
+ }
329
+
330
+ export interface FlagsManager {
331
+ are_flags_ready(): boolean;
332
+ get_variant(
333
+ featureName: string,
334
+ fallback: FlagsVariant
335
+ ): Promise<FlagsVariant>;
336
+ get_variant_sync(featureName: string, fallback: FlagsVariant): FlagsVariant;
337
+ get_variant_value(featureName: string, fallbackValue: any): Promise<any>;
338
+ get_variant_value_sync(featureName: string, fallbackValue: any): any;
339
+ is_enabled(featureName: string, fallbackValue?: boolean): Promise<boolean>;
340
+ is_enabled_sync(featureName: string, fallbackValue?: boolean): boolean;
341
+ update_context(
342
+ context: Dict,
343
+ options?: FlagsUpdateContextOptions
344
+ ): Promise<void>;
345
+ }
346
+
280
347
  export interface Mixpanel {
281
348
  add_group(group_key: string, group_id: string, callback?: Callback): void;
282
349
  alias(alias: string, original?: string): void;
283
350
  clear_opt_in_out_tracking(options?: Partial<ClearOptOutInOutOptions>): void;
284
351
  disable(events?: string[]): void;
352
+ flags: FlagsManager;
285
353
  get_config(prop_name?: string): any;
286
354
  get_distinct_id(): any;
287
355
  get_group(group_key: string, group_id: string): Group;
288
356
  get_property(property_name: string): any;
357
+ get_session_replay_url(): string;
289
358
  has_opted_in_tracking(options?: Partial<HasOptedInOutOptions>): boolean;
290
359
  has_opted_out_tracking(options?: Partial<HasOptedInOutOptions>): boolean;
291
360
  identify(unique_id?: string): any;
292
- init(token: string, config: Partial<Config>, name: string): Mixpanel;
361
+ init(token: string, config: Partial<Config>, name?: string): Mixpanel;
293
362
  opt_in_tracking(options?: Partial<InTrackingOptions>): void;
294
363
  opt_out_tracking(options?: Partial<OutTrackingOptions>): void;
295
364
  push(item: PushItem): void;
@@ -314,6 +383,7 @@ export interface Mixpanel {
314
383
  group_ids: string | string[] | number | number[],
315
384
  callback?: Callback
316
385
  ): void;
386
+ start_batch_senders(): void;
317
387
  time_event(event_name: string): void;
318
388
  track(
319
389
  event_name: string,
@@ -343,6 +413,7 @@ export interface Mixpanel {
343
413
  ): void;
344
414
  unregister(property: string, options?: Partial<RegisterOptions>): void;
345
415
  people: People;
416
+ start_batch_senders(): void;
346
417
  start_session_recording(): void;
347
418
  stop_session_recording(): void;
348
419
  get_session_recording_properties(): { $mp_replay_id?: string } | {};
@@ -57,8 +57,6 @@ 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';
@@ -152,6 +150,7 @@ var DEFAULT_CONFIG = {
152
150
  'record_block_selector': 'img, video, audio',
153
151
  'record_canvas': false,
154
152
  'record_collect_fonts': false,
153
+ 'record_console': true,
155
154
  'record_heatmap_data': false,
156
155
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
157
156
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
@@ -223,6 +222,17 @@ var create_mplib = function(token, config, name) {
223
222
  // global debug to be true
224
223
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
225
224
 
225
+ var source = init_type === INIT_MODULE ? 'module' : 'snippet';
226
+ window.dispatchEvent(new window.CustomEvent('$mp_sdk_to_extension_event', {
227
+ 'detail': {
228
+ 'instance': instance,
229
+ 'source': source,
230
+ 'token': token,
231
+ 'name': name,
232
+ 'info': _.info
233
+ }
234
+ }));
235
+
226
236
  // if target is not defined, we called init after the lib already
227
237
  // loaded, so there won't be an array of things to execute
228
238
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -293,6 +303,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
293
303
  }
294
304
  }
295
305
 
306
+ this.hooks = {};
307
+
296
308
  this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
297
309
  'name': name,
298
310
  'token': token,
@@ -893,7 +905,12 @@ MixpanelLib.prototype.init_batchers = function() {
893
905
  );
894
906
  }, this),
895
907
  beforeSendHook: _.bind(function(item) {
896
- return this._run_hook('before_send_' + attrs.type, item);
908
+ var ret = this._run_hook('before_send_' + attrs.type, item);
909
+ if (ret) {
910
+ return ret[0];
911
+ } else {
912
+ return null;
913
+ }
897
914
  }, this),
898
915
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
899
916
  usePersistence: true,
@@ -986,6 +1003,9 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
986
1003
  var send_request_immediately = _.bind(function() {
987
1004
  if (!send_request_options.skip_hooks) {
988
1005
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
1006
+ if (truncated_data) {
1007
+ truncated_data = truncated_data[0];
1008
+ }
989
1009
  }
990
1010
  if (truncated_data) {
991
1011
  console.log('MIXPANEL REQUEST:');
@@ -1040,6 +1060,17 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
1040
1060
  * with the tracking payload sent to the API server is returned; otherwise false.
1041
1061
  */
1042
1062
  MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) {
1063
+ var ret;
1064
+ if (!(options && options.skip_hooks)) {
1065
+ ret = this._run_hook('before_track', event_name, properties);
1066
+ if (ret === null) {
1067
+ return;
1068
+ } else {
1069
+ event_name = ret[0];
1070
+ properties = ret[1];
1071
+ }
1072
+ }
1073
+
1043
1074
  if (!callback && typeof options === 'function') {
1044
1075
  callback = options;
1045
1076
  options = null;
@@ -1109,7 +1140,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
1109
1140
  'event': event_name,
1110
1141
  'properties': properties
1111
1142
  };
1112
- var ret = this._track_or_batch({
1143
+ ret = this._track_or_batch({
1113
1144
  type: 'events',
1114
1145
  data: data,
1115
1146
  endpoint: this.get_api_host('events') + '/' + this.get_config('api_routes')['track'],
@@ -1455,6 +1486,14 @@ var options_for_register = function(days_or_options) {
1455
1486
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
1456
1487
  */
1457
1488
  MixpanelLib.prototype.register = function(props, days_or_options) {
1489
+ var ret = this._run_hook('before_register', props, days_or_options);
1490
+ if (ret === null) {
1491
+ return;
1492
+ } else {
1493
+ props = ret[0];
1494
+ days_or_options = ret[1];
1495
+ }
1496
+
1458
1497
  var options = options_for_register(days_or_options);
1459
1498
  if (options['persistent']) {
1460
1499
  this['persistence'].register(props, options['days']);
@@ -1491,6 +1530,15 @@ MixpanelLib.prototype.register = function(props, days_or_options) {
1491
1530
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
1492
1531
  */
1493
1532
  MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
1533
+ var ret = this._run_hook('before_register_once', props, default_value, days_or_options);
1534
+ if (ret === null) {
1535
+ return;
1536
+ } else {
1537
+ props = ret[0];
1538
+ default_value = ret[1];
1539
+ days_or_options = ret[2];
1540
+ }
1541
+
1494
1542
  var options = options_for_register(days_or_options);
1495
1543
  if (options['persistent']) {
1496
1544
  this['persistence'].register_once(props, default_value, options['days']);
@@ -1514,6 +1562,14 @@ MixpanelLib.prototype.register_once = function(props, default_value, days_or_opt
1514
1562
  * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
1515
1563
  */
1516
1564
  MixpanelLib.prototype.unregister = function(property, options) {
1565
+ var ret = this._run_hook('before_unregister', property, options);
1566
+ if (ret === null) {
1567
+ return;
1568
+ } else {
1569
+ property = ret[0];
1570
+ options = ret[1];
1571
+ }
1572
+
1517
1573
  options = options_for_register(options);
1518
1574
  if (options['persistent']) {
1519
1575
  this['persistence'].unregister(property);
@@ -1562,6 +1618,13 @@ MixpanelLib.prototype.identify = function(
1562
1618
  // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
1563
1619
  // _union_callback:function A callback to be run if and when the People union queue is flushed
1564
1620
  // _unset_callback:function A callback to be run if and when the People unset queue is flushed
1621
+ var ret = this._run_hook('before_identify', new_distinct_id);
1622
+
1623
+ if (ret === null) {
1624
+ return -1;
1625
+ } else {
1626
+ new_distinct_id = ret[0];
1627
+ }
1565
1628
 
1566
1629
  var previous_distinct_id = this.get_distinct_id();
1567
1630
  if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
@@ -1886,6 +1949,25 @@ MixpanelLib.prototype.set_config = function(config) {
1886
1949
  if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
1887
1950
  this.autocapture.init();
1888
1951
  }
1952
+
1953
+ if (_.isObject(config['hooks'])) {
1954
+ this.hooks = {};
1955
+ _.each(config['hooks'], function(hook_value, hook_name) {
1956
+ if (_.isFunction(hook_value)) {
1957
+ this.hooks[hook_name] = [hook_value];
1958
+ } else if (_.isArray(hook_value)) {
1959
+ this.hooks[hook_name] = [];
1960
+ for (var i = 0; i < hook_value.length; i++) {
1961
+ if (!_.isFunction(hook_value[i])) {
1962
+ console.critical('Invalid hook added. Hook is not a function');
1963
+ }
1964
+ this.hooks[hook_name].push(hook_value[i]);
1965
+ }
1966
+ } else {
1967
+ console.critical('Invalid hooks added. Ensure that the hook values passed into config.hooks are functions or arrays of functions.');
1968
+ }
1969
+ }, this);
1970
+ }
1889
1971
  }
1890
1972
  };
1891
1973
 
@@ -1903,12 +1985,26 @@ MixpanelLib.prototype.get_config = function(prop_name) {
1903
1985
  * @returns {any|null} return value of user-provided hook, or null if nothing was returned
1904
1986
  */
1905
1987
  MixpanelLib.prototype._run_hook = function(hook_name) {
1906
- var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
1907
- if (typeof ret === 'undefined') {
1908
- this.report_error(hook_name + ' hook did not return a value');
1909
- ret = null;
1910
- }
1911
- return ret;
1988
+ var hook_data = slice.call(arguments, 1);
1989
+ _.each(this.hooks[hook_name], function(hook) {
1990
+ if (hook_data === null) {
1991
+ return null;
1992
+ }
1993
+
1994
+ var ret = hook.apply(this, hook_data);
1995
+
1996
+ if (typeof ret === 'undefined') {
1997
+ this.report_error(hook_name + ' hook did not return a valid value');
1998
+ hook_data = null;
1999
+ } else {
2000
+ if (!_.isArray(ret)) {
2001
+ ret = [ret];
2002
+ }
2003
+ hook_data.splice.apply(hook_data, [0, ret.length].concat(ret));
2004
+ }
2005
+ }, this);
2006
+
2007
+ return hook_data;
1912
2008
  };
1913
2009
 
1914
2010
  /**
@@ -2219,6 +2315,25 @@ MixpanelLib.prototype.report_error = function(msg, err) {
2219
2315
  }
2220
2316
  };
2221
2317
 
2318
+ MixpanelLib.prototype.add_hook = function(hook_name, hook_fn) {
2319
+ if (!this.hooks[hook_name]) {
2320
+ this.hooks[hook_name] = [];
2321
+ }
2322
+ this.hooks[hook_name].push(hook_fn);
2323
+ };
2324
+
2325
+ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
2326
+ var fn_index;
2327
+ if (this.hooks[hook_name]) {
2328
+ fn_index = this.hooks[hook_name].indexOf(hook_fn);
2329
+ if (fn_index !== -1) {
2330
+ this.hooks[hook_name].splice(fn_index, 1);
2331
+ } else {
2332
+ console.log('remove_hook failed. Matching hook was not found');
2333
+ }
2334
+ }
2335
+ };
2336
+
2222
2337
  // EXPORTS (for closure compiler)
2223
2338
 
2224
2339
  // MixpanelLib Exports
@@ -2251,6 +2366,8 @@ MixpanelLib.prototype['get_group'] = MixpanelLib.protot
2251
2366
  MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group;
2252
2367
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
2253
2368
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
2369
+ MixpanelLib.prototype['add_hook'] = MixpanelLib.prototype.add_hook;
2370
+ MixpanelLib.prototype['remove_hook'] = MixpanelLib.prototype.remove_hook;
2254
2371
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
2255
2372
  MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
2256
2373
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
@@ -1,4 +1,4 @@
1
- import { record } from '@mixpanel/rrweb';
1
+ import { record } from './rrweb-entrypoint';
2
2
  import { Promise } from '../promise-polyfill';
3
3
  import { SessionRecording } from './session-recording';
4
4
  import { RecordingRegistry } from './recording-registry';
@@ -0,0 +1,6 @@
1
+ // this file exists as an entry point to be able to transpile rrweb packages to es5
2
+ // compatible code without needing to transpile the entire mixpanel-js codebase
3
+ import {record, EventType, IncrementalSource} from '@mixpanel/rrweb';
4
+ import {getRecordConsolePlugin} from '@mixpanel/rrweb-plugin-console-record';
5
+
6
+ export { record, EventType, IncrementalSource, getRecordConsolePlugin };