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
@@ -22,3 +22,4 @@ jobs:
22
22
  node-version: ${{ matrix.node-version }}
23
23
  - run: npm ci
24
24
  - run: npm test
25
+ - run: npm run build-dist
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ **2.73.0** (23 Dec 2025)
2
+ - Adds several new hooks: `before_identify`, `before_register`, `before_register_once`, `before_track`, `before_unregister`
3
+ - Adds instance-initialization notification to allow Data Inspector browser extension to hook into SDK actions
4
+ - Fixes and extends type definitions
5
+
6
+ **2.72.0** (14 Nov 2025)
7
+ - Adds Autocapture rage-click configuration option `interactive_elements_only`, to ignore clicks on non-interactive page elements such as text. Configure with: `mixpanel.init('<TOKEN>', {autocapture: {rage_click: {interactive_elements_only: true}}})`
8
+ - Adds TypeScript types for Feature Flags subsystem (`mixpanel.flags`)
9
+ - Adds JS console data to Session Recordings, enabled by default but configurable via the `record_console` initialization option.
10
+ - Fixes an issue in session recording where closing and opening a page would upload a replay shorter than the configured minimum duration (`record_min_ms`)
11
+ - Fixes an issue in session recording where payloads get truncated on old Safari versions due to a bug in their CompressionStream implementation
12
+
1
13
  **2.71.1** (30 Oct 2025)
2
14
  - fixes issue with $mp_page_leave events getting tracked when `record_heatmap_data` is on and there was no session recording taking place.
3
15
 
@@ -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 } | {};
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.71.1'
5
+ LIB_VERSION: '2.73.0'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -22,7 +22,9 @@ if (typeof(window) === 'undefined') {
22
22
  screen: { width: 0, height: 0 },
23
23
  location: loc,
24
24
  addEventListener: function() {},
25
- removeEventListener: function() {}
25
+ removeEventListener: function() {},
26
+ dispatchEvent: function() {},
27
+ CustomEvent: function () {}
26
28
  };
27
29
  } else {
28
30
  win = window;
@@ -2892,20 +2894,65 @@ function isDefinitelyNonInteractive(element) {
2892
2894
  return false;
2893
2895
  }
2894
2896
 
2897
+ /**
2898
+ * Get the composed path of a click event for elements embedded in shadow DOM.
2899
+ * @param {Event} event - event to get the composed path from
2900
+ * @returns {Array} the composed path of the click event
2901
+ */
2902
+ function getClickEventComposedPath(event) {
2903
+ if ('composedPath' in event) {
2904
+ return event['composedPath']();
2905
+ }
2906
+
2907
+ return [];
2908
+ }
2909
+
2910
+ /**
2911
+ * Get the element from a click event, accounting for elements embedded in shadow DOM.
2912
+ * @param {Event} event - event to get the target from
2913
+ * @returns {Element | null} the element that was the target of the click event
2914
+ */
2915
+ function getClickEventTargetElement(event) {
2916
+ var path = getClickEventComposedPath(event);
2917
+
2918
+ if (path && path.length > 0) {
2919
+ return path[0];
2920
+ }
2921
+
2922
+ return event['target'] || event['srcElement'];
2923
+ }
2924
+
2895
2925
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
2896
2926
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
2897
2927
  /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
2928
+ /** @const */ var DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY = false;
2898
2929
 
2899
2930
  function RageClickTracker() {
2900
2931
  this.clicks = [];
2901
2932
  }
2902
2933
 
2903
- RageClickTracker.prototype.isRageClick = function(x, y, options) {
2934
+ /**
2935
+ * Determines if a click event is part of a rage click sequence.
2936
+ * @param {Event} event - the original click event.
2937
+ * @param {import('../index.d.ts').RageClickConfig} options - configuration options for rage click detection.
2938
+ * @returns {boolean} - true if the click is considered a rage click, false otherwise.
2939
+ */
2940
+ RageClickTracker.prototype.isRageClick = function(event, options) {
2904
2941
  options = options || {};
2905
2942
  var thresholdPx = options['threshold_px'] || DEFAULT_RAGE_CLICK_THRESHOLD_PX;
2906
2943
  var timeoutMs = options['timeout_ms'] || DEFAULT_RAGE_CLICK_TIMEOUT_MS;
2907
2944
  var clickCount = options['click_count'] || DEFAULT_RAGE_CLICK_CLICK_COUNT;
2945
+ var interactiveElementsOnly = options['interactive_elements_only'] || DEFAULT_RAGE_CLICK_INTERACTIVE_ELEMENTS_ONLY;
2946
+
2947
+ if (interactiveElementsOnly) {
2948
+ var target = getClickEventTargetElement(event);
2949
+ if (!target || isDefinitelyNonInteractive(target)) {
2950
+ return false;
2951
+ }
2952
+ }
2953
+
2908
2954
  var timestamp = Date.now();
2955
+ var x = event['pageX'], y = event['pageY'];
2909
2956
 
2910
2957
  var lastClick = this.clicks[this.clicks.length - 1];
2911
2958
  if (
@@ -2936,28 +2983,16 @@ ShadowDOMObserver.prototype.getEventTarget = function(event) {
2936
2983
  if (!this.observedShadowRoots) {
2937
2984
  return;
2938
2985
  }
2939
- var path = this.getComposedPath(event);
2940
- if (path && path.length) {
2941
- return path[0];
2942
- }
2943
2986
 
2944
- return event['target'] || event['srcElement'];
2987
+ return getClickEventTargetElement(event);
2945
2988
  };
2946
2989
 
2947
-
2948
- ShadowDOMObserver.prototype.getComposedPath = function(event) {
2949
- if ('composedPath' in event) {
2950
- return event['composedPath']();
2951
- }
2952
-
2953
- return [];
2954
- };
2955
2990
  ShadowDOMObserver.prototype.observeFromEvent = function(event) {
2956
2991
  if (!this.observedShadowRoots) {
2957
2992
  return;
2958
2993
  }
2959
2994
 
2960
- var path = this.getComposedPath(event);
2995
+ var path = getClickEventComposedPath(event);
2961
2996
 
2962
2997
  // Check each element in path for shadow roots
2963
2998
  for (var i = 0; i < path.length; i++) {
@@ -3709,7 +3744,7 @@ Autocapture.prototype.initRageClickTracking = function() {
3709
3744
  return;
3710
3745
  }
3711
3746
 
3712
- if (this._rageClickTracker.isRageClick(ev['pageX'], ev['pageY'], currentRageClickConfig)) {
3747
+ if (this._rageClickTracker.isRageClick(ev, currentRageClickConfig)) {
3713
3748
  this.trackDomEvent(ev, MP_EV_RAGE_CLICK);
3714
3749
  }
3715
3750
  }.bind(this);
@@ -6802,8 +6837,6 @@ var mixpanel_master; // main mixpanel instance / object
6802
6837
  var INIT_MODULE = 0;
6803
6838
  var INIT_SNIPPET = 1;
6804
6839
 
6805
- var IDENTITY_FUNC = function(x) {return x;};
6806
-
6807
6840
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
6808
6841
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
6809
6842
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
@@ -6897,6 +6930,7 @@ var DEFAULT_CONFIG = {
6897
6930
  'record_block_selector': 'img, video, audio',
6898
6931
  'record_canvas': false,
6899
6932
  'record_collect_fonts': false,
6933
+ 'record_console': true,
6900
6934
  'record_heatmap_data': false,
6901
6935
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
6902
6936
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
@@ -6968,6 +7002,17 @@ var create_mplib = function(token, config, name) {
6968
7002
  // global debug to be true
6969
7003
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
6970
7004
 
7005
+ var source = init_type === INIT_MODULE ? 'module' : 'snippet';
7006
+ win.dispatchEvent(new win.CustomEvent('$mp_sdk_to_extension_event', {
7007
+ 'detail': {
7008
+ 'instance': instance,
7009
+ 'source': source,
7010
+ 'token': token,
7011
+ 'name': name,
7012
+ 'info': _.info
7013
+ }
7014
+ }));
7015
+
6971
7016
  // if target is not defined, we called init after the lib already
6972
7017
  // loaded, so there won't be an array of things to execute
6973
7018
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -7038,6 +7083,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
7038
7083
  }
7039
7084
  }
7040
7085
 
7086
+ this.hooks = {};
7087
+
7041
7088
  this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
7042
7089
  'name': name,
7043
7090
  'token': token,
@@ -7638,7 +7685,12 @@ MixpanelLib.prototype.init_batchers = function() {
7638
7685
  );
7639
7686
  }, this),
7640
7687
  beforeSendHook: _.bind(function(item) {
7641
- return this._run_hook('before_send_' + attrs.type, item);
7688
+ var ret = this._run_hook('before_send_' + attrs.type, item);
7689
+ if (ret) {
7690
+ return ret[0];
7691
+ } else {
7692
+ return null;
7693
+ }
7642
7694
  }, this),
7643
7695
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
7644
7696
  usePersistence: true,
@@ -7731,6 +7783,9 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
7731
7783
  var send_request_immediately = _.bind(function() {
7732
7784
  if (!send_request_options.skip_hooks) {
7733
7785
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
7786
+ if (truncated_data) {
7787
+ truncated_data = truncated_data[0];
7788
+ }
7734
7789
  }
7735
7790
  if (truncated_data) {
7736
7791
  console.log('MIXPANEL REQUEST:');
@@ -7785,6 +7840,17 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
7785
7840
  * with the tracking payload sent to the API server is returned; otherwise false.
7786
7841
  */
7787
7842
  MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, options, callback) {
7843
+ var ret;
7844
+ if (!(options && options.skip_hooks)) {
7845
+ ret = this._run_hook('before_track', event_name, properties);
7846
+ if (ret === null) {
7847
+ return;
7848
+ } else {
7849
+ event_name = ret[0];
7850
+ properties = ret[1];
7851
+ }
7852
+ }
7853
+
7788
7854
  if (!callback && typeof options === 'function') {
7789
7855
  callback = options;
7790
7856
  options = null;
@@ -7854,7 +7920,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
7854
7920
  'event': event_name,
7855
7921
  'properties': properties
7856
7922
  };
7857
- var ret = this._track_or_batch({
7923
+ ret = this._track_or_batch({
7858
7924
  type: 'events',
7859
7925
  data: data,
7860
7926
  endpoint: this.get_api_host('events') + '/' + this.get_config('api_routes')['track'],
@@ -8200,6 +8266,14 @@ var options_for_register = function(days_or_options) {
8200
8266
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
8201
8267
  */
8202
8268
  MixpanelLib.prototype.register = function(props, days_or_options) {
8269
+ var ret = this._run_hook('before_register', props, days_or_options);
8270
+ if (ret === null) {
8271
+ return;
8272
+ } else {
8273
+ props = ret[0];
8274
+ days_or_options = ret[1];
8275
+ }
8276
+
8203
8277
  var options = options_for_register(days_or_options);
8204
8278
  if (options['persistent']) {
8205
8279
  this['persistence'].register(props, options['days']);
@@ -8236,6 +8310,15 @@ MixpanelLib.prototype.register = function(props, days_or_options) {
8236
8310
  * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
8237
8311
  */
8238
8312
  MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
8313
+ var ret = this._run_hook('before_register_once', props, default_value, days_or_options);
8314
+ if (ret === null) {
8315
+ return;
8316
+ } else {
8317
+ props = ret[0];
8318
+ default_value = ret[1];
8319
+ days_or_options = ret[2];
8320
+ }
8321
+
8239
8322
  var options = options_for_register(days_or_options);
8240
8323
  if (options['persistent']) {
8241
8324
  this['persistence'].register_once(props, default_value, options['days']);
@@ -8259,6 +8342,14 @@ MixpanelLib.prototype.register_once = function(props, default_value, days_or_opt
8259
8342
  * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
8260
8343
  */
8261
8344
  MixpanelLib.prototype.unregister = function(property, options) {
8345
+ var ret = this._run_hook('before_unregister', property, options);
8346
+ if (ret === null) {
8347
+ return;
8348
+ } else {
8349
+ property = ret[0];
8350
+ options = ret[1];
8351
+ }
8352
+
8262
8353
  options = options_for_register(options);
8263
8354
  if (options['persistent']) {
8264
8355
  this['persistence'].unregister(property);
@@ -8307,6 +8398,13 @@ MixpanelLib.prototype.identify = function(
8307
8398
  // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
8308
8399
  // _union_callback:function A callback to be run if and when the People union queue is flushed
8309
8400
  // _unset_callback:function A callback to be run if and when the People unset queue is flushed
8401
+ var ret = this._run_hook('before_identify', new_distinct_id);
8402
+
8403
+ if (ret === null) {
8404
+ return -1;
8405
+ } else {
8406
+ new_distinct_id = ret[0];
8407
+ }
8310
8408
 
8311
8409
  var previous_distinct_id = this.get_distinct_id();
8312
8410
  if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
@@ -8631,6 +8729,25 @@ MixpanelLib.prototype.set_config = function(config) {
8631
8729
  if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
8632
8730
  this.autocapture.init();
8633
8731
  }
8732
+
8733
+ if (_.isObject(config['hooks'])) {
8734
+ this.hooks = {};
8735
+ _.each(config['hooks'], function(hook_value, hook_name) {
8736
+ if (_.isFunction(hook_value)) {
8737
+ this.hooks[hook_name] = [hook_value];
8738
+ } else if (_.isArray(hook_value)) {
8739
+ this.hooks[hook_name] = [];
8740
+ for (var i = 0; i < hook_value.length; i++) {
8741
+ if (!_.isFunction(hook_value[i])) {
8742
+ console.critical('Invalid hook added. Hook is not a function');
8743
+ }
8744
+ this.hooks[hook_name].push(hook_value[i]);
8745
+ }
8746
+ } else {
8747
+ console.critical('Invalid hooks added. Ensure that the hook values passed into config.hooks are functions or arrays of functions.');
8748
+ }
8749
+ }, this);
8750
+ }
8634
8751
  }
8635
8752
  };
8636
8753
 
@@ -8648,12 +8765,26 @@ MixpanelLib.prototype.get_config = function(prop_name) {
8648
8765
  * @returns {any|null} return value of user-provided hook, or null if nothing was returned
8649
8766
  */
8650
8767
  MixpanelLib.prototype._run_hook = function(hook_name) {
8651
- var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
8652
- if (typeof ret === 'undefined') {
8653
- this.report_error(hook_name + ' hook did not return a value');
8654
- ret = null;
8655
- }
8656
- return ret;
8768
+ var hook_data = slice.call(arguments, 1);
8769
+ _.each(this.hooks[hook_name], function(hook) {
8770
+ if (hook_data === null) {
8771
+ return null;
8772
+ }
8773
+
8774
+ var ret = hook.apply(this, hook_data);
8775
+
8776
+ if (typeof ret === 'undefined') {
8777
+ this.report_error(hook_name + ' hook did not return a valid value');
8778
+ hook_data = null;
8779
+ } else {
8780
+ if (!_.isArray(ret)) {
8781
+ ret = [ret];
8782
+ }
8783
+ hook_data.splice.apply(hook_data, [0, ret.length].concat(ret));
8784
+ }
8785
+ }, this);
8786
+
8787
+ return hook_data;
8657
8788
  };
8658
8789
 
8659
8790
  /**
@@ -8964,6 +9095,25 @@ MixpanelLib.prototype.report_error = function(msg, err) {
8964
9095
  }
8965
9096
  };
8966
9097
 
9098
+ MixpanelLib.prototype.add_hook = function(hook_name, hook_fn) {
9099
+ if (!this.hooks[hook_name]) {
9100
+ this.hooks[hook_name] = [];
9101
+ }
9102
+ this.hooks[hook_name].push(hook_fn);
9103
+ };
9104
+
9105
+ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
9106
+ var fn_index;
9107
+ if (this.hooks[hook_name]) {
9108
+ fn_index = this.hooks[hook_name].indexOf(hook_fn);
9109
+ if (fn_index !== -1) {
9110
+ this.hooks[hook_name].splice(fn_index, 1);
9111
+ } else {
9112
+ console.log('remove_hook failed. Matching hook was not found');
9113
+ }
9114
+ }
9115
+ };
9116
+
8967
9117
  // EXPORTS (for closure compiler)
8968
9118
 
8969
9119
  // MixpanelLib Exports
@@ -8996,6 +9146,8 @@ MixpanelLib.prototype['get_group'] = MixpanelLib.protot
8996
9146
  MixpanelLib.prototype['set_group'] = MixpanelLib.prototype.set_group;
8997
9147
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
8998
9148
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
9149
+ MixpanelLib.prototype['add_hook'] = MixpanelLib.prototype.add_hook;
9150
+ MixpanelLib.prototype['remove_hook'] = MixpanelLib.prototype.remove_hook;
8999
9151
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
9000
9152
  MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
9001
9153
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;