mixpanel-browser 2.66.0 → 2.68.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.
@@ -13948,7 +13948,7 @@
13948
13948
 
13949
13949
  var Config = {
13950
13950
  DEBUG: false,
13951
- LIB_VERSION: '2.66.0'
13951
+ LIB_VERSION: '2.68.0'
13952
13952
  };
13953
13953
 
13954
13954
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -17066,6 +17066,13 @@
17066
17066
  * @property {string} replayStartUrl
17067
17067
  */
17068
17068
 
17069
+ /**
17070
+ * @typedef {Object} UserIdInfo
17071
+ * @property {string} distinct_id
17072
+ * @property {string} user_id
17073
+ * @property {string} device_id
17074
+ */
17075
+
17069
17076
 
17070
17077
  /**
17071
17078
  * This class encapsulates a single session recording and its lifecycle.
@@ -17121,6 +17128,30 @@
17121
17128
  });
17122
17129
  };
17123
17130
 
17131
+ /**
17132
+ * @returns {UserIdInfo}
17133
+ */
17134
+ SessionRecording.prototype.getUserIdInfo = function () {
17135
+ if (this.finalFlushUserIdInfo) {
17136
+ return this.finalFlushUserIdInfo;
17137
+ }
17138
+
17139
+ var userIdInfo = {
17140
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
17141
+ };
17142
+
17143
+ // send ID management props if they exist
17144
+ var deviceId = this._mixpanel.get_property('$device_id');
17145
+ if (deviceId) {
17146
+ userIdInfo['$device_id'] = deviceId;
17147
+ }
17148
+ var userId = this._mixpanel.get_property('$user_id');
17149
+ if (userId) {
17150
+ userIdInfo['$user_id'] = userId;
17151
+ }
17152
+ return userIdInfo;
17153
+ };
17154
+
17124
17155
  SessionRecording.prototype.unloadPersistedData = function () {
17125
17156
  this.batcher.stop();
17126
17157
  return this.batcher.flush()
@@ -17245,6 +17276,9 @@
17245
17276
  };
17246
17277
 
17247
17278
  SessionRecording.prototype.stopRecording = function (skipFlush) {
17279
+ // store the user ID info in case this is getting called in mixpanel.reset()
17280
+ this.finalFlushUserIdInfo = this.getUserIdInfo();
17281
+
17248
17282
  if (!this.isRrwebStopped()) {
17249
17283
  try {
17250
17284
  this._stopRecording();
@@ -17410,7 +17444,6 @@
17410
17444
  '$current_url': this.batchStartUrl,
17411
17445
  '$lib_version': Config.LIB_VERSION,
17412
17446
  'batch_start_time': batchStartTime / 1000,
17413
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
17414
17447
  'mp_lib': 'web',
17415
17448
  'replay_id': replayId,
17416
17449
  'replay_length_ms': replayLengthMs,
@@ -17419,16 +17452,7 @@
17419
17452
  'seq': this.seqNo
17420
17453
  };
17421
17454
  var eventsJson = JSON.stringify(data);
17422
-
17423
- // send ID management props if they exist
17424
- var deviceId = this._mixpanel.get_property('$device_id');
17425
- if (deviceId) {
17426
- reqParams['$device_id'] = deviceId;
17427
- }
17428
- var userId = this._mixpanel.get_property('$user_id');
17429
- if (userId) {
17430
- reqParams['$user_id'] = userId;
17431
- }
17455
+ Object.assign(reqParams, this.getUserIdInfo());
17432
17456
 
17433
17457
  if (CompressionStream) {
17434
17458
  var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
@@ -18209,6 +18233,38 @@
18209
18233
  return true;
18210
18234
  }
18211
18235
 
18236
+ /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
18237
+ /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
18238
+ /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
18239
+
18240
+ function RageClickTracker() {
18241
+ this.clicks = [];
18242
+ }
18243
+
18244
+ RageClickTracker.prototype.isRageClick = function(x, y, options) {
18245
+ options = options || {};
18246
+ var thresholdPx = options['threshold_px'] || DEFAULT_RAGE_CLICK_THRESHOLD_PX;
18247
+ var timeoutMs = options['timeout_ms'] || DEFAULT_RAGE_CLICK_TIMEOUT_MS;
18248
+ var clickCount = options['click_count'] || DEFAULT_RAGE_CLICK_CLICK_COUNT;
18249
+ var timestamp = Date.now();
18250
+
18251
+ var lastClick = this.clicks[this.clicks.length - 1];
18252
+ if (
18253
+ lastClick &&
18254
+ timestamp - lastClick.timestamp < timeoutMs &&
18255
+ Math.sqrt(Math.pow(x - lastClick.x, 2) + Math.pow(y - lastClick.y, 2)) < thresholdPx
18256
+ ) {
18257
+ this.clicks.push({ x: x, y: y, timestamp: timestamp });
18258
+ if (this.clicks.length >= clickCount) {
18259
+ this.clicks = [];
18260
+ return true;
18261
+ }
18262
+ } else {
18263
+ this.clicks = [{ x: x, y: y, timestamp: timestamp }];
18264
+ }
18265
+ return false;
18266
+ };
18267
+
18212
18268
  var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
18213
18269
  var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
18214
18270
 
@@ -18230,6 +18286,7 @@
18230
18286
  var CONFIG_TRACK_CLICK = 'click';
18231
18287
  var CONFIG_TRACK_INPUT = 'input';
18232
18288
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
18289
+ var CONFIG_TRACK_RAGE_CLICK = 'rage_click';
18233
18290
  var CONFIG_TRACK_SCROLL = 'scroll';
18234
18291
  var CONFIG_TRACK_SUBMIT = 'submit';
18235
18292
 
@@ -18247,6 +18304,7 @@
18247
18304
  CONFIG_DEFAULTS$1[CONFIG_TRACK_CLICK] = true;
18248
18305
  CONFIG_DEFAULTS$1[CONFIG_TRACK_INPUT] = true;
18249
18306
  CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
18307
+ CONFIG_DEFAULTS$1[CONFIG_TRACK_RAGE_CLICK] = true;
18250
18308
  CONFIG_DEFAULTS$1[CONFIG_TRACK_SCROLL] = true;
18251
18309
  CONFIG_DEFAULTS$1[CONFIG_TRACK_SUBMIT] = true;
18252
18310
 
@@ -18256,6 +18314,7 @@
18256
18314
 
18257
18315
  var MP_EV_CLICK = '$mp_click';
18258
18316
  var MP_EV_INPUT = '$mp_input_change';
18317
+ var MP_EV_RAGE_CLICK = '$mp_rage_click';
18259
18318
  var MP_EV_SCROLL = '$mp_scroll';
18260
18319
  var MP_EV_SUBMIT = '$mp_submit';
18261
18320
 
@@ -18278,6 +18337,7 @@
18278
18337
  this.initInputTracking();
18279
18338
  this.initScrollTracking();
18280
18339
  this.initSubmitTracking();
18340
+ this.initRageClickTracking();
18281
18341
  };
18282
18342
 
18283
18343
  Autocapture.prototype.getFullConfig = function() {
@@ -18356,6 +18416,11 @@
18356
18416
  return;
18357
18417
  }
18358
18418
 
18419
+ var isCapturedForHeatMap = this.mp.is_recording_heatmap_data() && (
18420
+ (mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK)) ||
18421
+ (mpEventName === MP_EV_RAGE_CLICK && !this._getRageClickConfig())
18422
+ );
18423
+
18359
18424
  var props = getPropsForDOMEvent(ev, {
18360
18425
  allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
18361
18426
  allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
@@ -18364,7 +18429,7 @@
18364
18429
  blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
18365
18430
  captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
18366
18431
  captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT),
18367
- capturedForHeatMap: mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK) && this.mp.is_recording_heatmap_data(),
18432
+ capturedForHeatMap: isCapturedForHeatMap,
18368
18433
  });
18369
18434
  if (props) {
18370
18435
  _.extend(props, DEFAULT_PROPS);
@@ -18372,6 +18437,24 @@
18372
18437
  }
18373
18438
  };
18374
18439
 
18440
+ Autocapture.prototype._getRageClickConfig = function() {
18441
+ var config = this.getConfig(CONFIG_TRACK_RAGE_CLICK);
18442
+
18443
+ if (!config) {
18444
+ return null; // rage click tracking disabled
18445
+ }
18446
+
18447
+ if (config === true) {
18448
+ return {}; // use defaults
18449
+ }
18450
+
18451
+ if (typeof config === 'object') {
18452
+ return config; // use custom configuration
18453
+ }
18454
+
18455
+ return {}; // fallback to defaults for any other truthy value
18456
+ };
18457
+
18375
18458
  Autocapture.prototype.initClickTracking = function() {
18376
18459
  win.removeEventListener(EV_CLICK, this.listenerClick);
18377
18460
 
@@ -18473,6 +18556,36 @@
18473
18556
  }.bind(this)));
18474
18557
  };
18475
18558
 
18559
+ Autocapture.prototype.initRageClickTracking = function() {
18560
+ win.removeEventListener(EV_CLICK, this.listenerRageClick);
18561
+
18562
+ var rageClickConfig = this._getRageClickConfig();
18563
+ if (!rageClickConfig && !this.mp.get_config('record_heatmap_data')) {
18564
+ return;
18565
+ }
18566
+
18567
+ logger$1.log('Initializing rage click tracking');
18568
+ if (!this._rageClickTracker) {
18569
+ this._rageClickTracker = new RageClickTracker();
18570
+ }
18571
+
18572
+ this.listenerRageClick = function(ev) {
18573
+ var currentRageClickConfig = this._getRageClickConfig();
18574
+ if (!currentRageClickConfig && !this.mp.is_recording_heatmap_data()) {
18575
+ return;
18576
+ }
18577
+
18578
+ if (this.currentUrlBlocked()) {
18579
+ return;
18580
+ }
18581
+
18582
+ if (this._rageClickTracker.isRageClick(ev['pageX'], ev['pageY'], currentRageClickConfig)) {
18583
+ this.trackDomEvent(ev, MP_EV_RAGE_CLICK);
18584
+ }
18585
+ }.bind(this);
18586
+ win.addEventListener(EV_CLICK, this.listenerRageClick);
18587
+ };
18588
+
18476
18589
  Autocapture.prototype.initScrollTracking = function() {
18477
18590
  win.removeEventListener(EV_SCROLLEND, this.listenerScroll);
18478
18591
 
@@ -18557,8 +18670,10 @@
18557
18670
  * @constructor
18558
18671
  */
18559
18672
  var FeatureFlagManager = function(initOptions) {
18673
+ this.getFullApiRoute = initOptions.getFullApiRoute;
18560
18674
  this.getMpConfig = initOptions.getConfigFunc;
18561
- this.getDistinctId = initOptions.getDistinctIdFunc;
18675
+ this.setMpConfig = initOptions.setConfigFunc;
18676
+ this.getMpProperty = initOptions.getPropertyFunc;
18562
18677
  this.track = initOptions.trackingFunc;
18563
18678
  };
18564
18679
 
@@ -18595,6 +18710,23 @@
18595
18710
  return !!this.getMpConfig(FLAGS_CONFIG_KEY);
18596
18711
  };
18597
18712
 
18713
+ FeatureFlagManager.prototype.updateContext = function(newContext, options) {
18714
+ if (!this.isSystemEnabled()) {
18715
+ logger.critical('Feature Flags not enabled, cannot update context');
18716
+ return Promise.resolve();
18717
+ }
18718
+
18719
+ var ffConfig = this.getMpConfig(FLAGS_CONFIG_KEY);
18720
+ if (!_.isObject(ffConfig)) {
18721
+ ffConfig = {};
18722
+ }
18723
+ var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
18724
+ ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
18725
+
18726
+ this.setMpConfig(FLAGS_CONFIG_KEY, ffConfig);
18727
+ return this.fetchFlags();
18728
+ };
18729
+
18598
18730
  FeatureFlagManager.prototype.areFlagsReady = function() {
18599
18731
  if (!this.isSystemEnabled()) {
18600
18732
  logger.error('Feature Flags not enabled');
@@ -18604,15 +18736,17 @@
18604
18736
 
18605
18737
  FeatureFlagManager.prototype.fetchFlags = function() {
18606
18738
  if (!this.isSystemEnabled()) {
18607
- return;
18739
+ return Promise.resolve();
18608
18740
  }
18609
18741
 
18610
- var distinctId = this.getDistinctId();
18742
+ var distinctId = this.getMpProperty('distinct_id');
18743
+ var deviceId = this.getMpProperty('$device_id');
18611
18744
  logger.log('Fetching flags for distinct ID: ' + distinctId);
18612
18745
  var reqParams = {
18613
- 'context': _.extend({'distinct_id': distinctId}, this.getConfig(CONFIG_CONTEXT))
18746
+ 'context': _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT))
18614
18747
  };
18615
- this.fetchPromise = win['fetch'](this.getMpConfig('api_host') + '/' + this.getMpConfig('api_routes')['flags'], {
18748
+ this._fetchInProgressStartTime = Date.now();
18749
+ this.fetchPromise = win['fetch'](this.getFullApiRoute(), {
18616
18750
  'method': 'POST',
18617
18751
  'headers': {
18618
18752
  'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
@@ -18620,6 +18754,7 @@
18620
18754
  },
18621
18755
  'body': JSON.stringify(reqParams)
18622
18756
  }).then(function(response) {
18757
+ this.markFetchComplete();
18623
18758
  return response.json().then(function(responseBody) {
18624
18759
  var responseFlags = responseBody['flags'];
18625
18760
  if (!responseFlags) {
@@ -18634,9 +18769,26 @@
18634
18769
  });
18635
18770
  this.flags = flags;
18636
18771
  }.bind(this)).catch(function(error) {
18772
+ this.markFetchComplete();
18637
18773
  logger.error(error);
18638
- });
18639
- }.bind(this)).catch(function() {});
18774
+ }.bind(this));
18775
+ }.bind(this)).catch(function(error) {
18776
+ this.markFetchComplete();
18777
+ logger.error(error);
18778
+ }.bind(this));
18779
+
18780
+ return this.fetchPromise;
18781
+ };
18782
+
18783
+ FeatureFlagManager.prototype.markFetchComplete = function() {
18784
+ if (!this._fetchInProgressStartTime) {
18785
+ logger.error('Fetch in progress started time not set, cannot mark fetch complete');
18786
+ return;
18787
+ }
18788
+ this._fetchStartTime = this._fetchInProgressStartTime;
18789
+ this._fetchCompleteTime = Date.now();
18790
+ this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
18791
+ this._fetchInProgressStartTime = null;
18640
18792
  };
18641
18793
 
18642
18794
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
@@ -18715,7 +18867,10 @@
18715
18867
  this.track('$experiment_started', {
18716
18868
  'Experiment name': featureName,
18717
18869
  'Variant name': feature['key'],
18718
- '$experiment_type': 'feature_flag'
18870
+ '$experiment_type': 'feature_flag',
18871
+ 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
18872
+ 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
18873
+ 'Variant fetch latency (ms)': this._fetchLatency
18719
18874
  });
18720
18875
  };
18721
18876
 
@@ -18735,6 +18890,7 @@
18735
18890
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
18736
18891
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
18737
18892
  FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
18893
+ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
18738
18894
 
18739
18895
  // Deprecated method
18740
18896
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
@@ -20195,7 +20351,7 @@
20195
20351
  'batch_autostart': true,
20196
20352
  'hooks': {},
20197
20353
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
20198
- 'record_block_selector': 'img, video',
20354
+ 'record_block_selector': 'img, video, audio',
20199
20355
  'record_canvas': false,
20200
20356
  'record_collect_fonts': false,
20201
20357
  'record_heatmap_data': false,
@@ -20415,8 +20571,12 @@
20415
20571
  }
20416
20572
 
20417
20573
  this.flags = new FeatureFlagManager({
20574
+ getFullApiRoute: _.bind(function() {
20575
+ return this.get_api_host('flags') + '/' + this.get_config('api_routes')['flags'];
20576
+ }, this),
20418
20577
  getConfigFunc: _.bind(this.get_config, this),
20419
- getDistinctIdFunc: _.bind(this.get_distinct_id, this),
20578
+ setConfigFunc: _.bind(this.set_config, this),
20579
+ getPropertyFunc: _.bind(this.get_property, this),
20420
20580
  trackingFunc: _.bind(this.track, this)
20421
20581
  });
20422
20582
  this.flags.init();
@@ -20902,11 +21062,10 @@
20902
21062
 
20903
21063
  MixpanelLib.prototype.get_batcher_configs = function() {
20904
21064
  var queue_prefix = '__mpq_' + this.get_config('token');
20905
- var api_routes = this.get_config('api_routes');
20906
21065
  this._batcher_configs = this._batcher_configs || {
20907
- events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'},
20908
- people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'},
20909
- groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'}
21066
+ events: {type: 'events', api_name: 'track', queue_key: queue_prefix + '_ev'},
21067
+ people: {type: 'people', api_name: 'engage', queue_key: queue_prefix + '_pp'},
21068
+ groups: {type: 'groups', api_name: 'groups', queue_key: queue_prefix + '_gr'}
20910
21069
  };
20911
21070
  return this._batcher_configs;
20912
21071
  };
@@ -20920,8 +21079,9 @@
20920
21079
  libConfig: this['config'],
20921
21080
  errorReporter: this.get_config('error_reporter'),
20922
21081
  sendRequestFunc: _.bind(function(data, options, cb) {
21082
+ var api_routes = this.get_config('api_routes');
20923
21083
  this._send_request(
20924
- this.get_config('api_host') + attrs.endpoint,
21084
+ this.get_api_host(attrs.api_name) + '/' + api_routes[attrs.api_name],
20925
21085
  this._encode_data_for_request(data),
20926
21086
  options,
20927
21087
  this._prepare_callback(cb, data)
@@ -21649,31 +21809,15 @@
21649
21809
  * Useful for clearing data when a user logs out.
21650
21810
  */
21651
21811
  MixpanelLib.prototype.reset = function() {
21652
- var self = this;
21653
-
21654
- var reset = function () {
21655
- self['persistence'].clear();
21656
- self._flags.identify_called = false;
21657
- var uuid = _.UUID();
21658
- self.register_once({
21659
- 'distinct_id': DEVICE_ID_PREFIX + uuid,
21660
- '$device_id': uuid
21661
- }, '');
21662
- };
21663
-
21664
- if (self._recorder) {
21665
- self.stop_session_recording()
21666
- .then(function () {
21667
- reset();
21668
- self._check_and_start_session_recording();
21669
- })
21670
- .catch(_.bind(function (err) {
21671
- reset();
21672
- this.report_error('Error restarting recording session', err);
21673
- }, this));
21674
- } else {
21675
- reset();
21676
- }
21812
+ this.stop_session_recording();
21813
+ this['persistence'].clear();
21814
+ this._flags.identify_called = false;
21815
+ var uuid = _.UUID();
21816
+ this.register_once({
21817
+ 'distinct_id': DEVICE_ID_PREFIX + uuid,
21818
+ '$device_id': uuid
21819
+ }, '');
21820
+ this._check_and_start_session_recording();
21677
21821
  };
21678
21822
 
21679
21823
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.66.0",
3
+ "version": "2.68.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",
@@ -5,6 +5,7 @@ import {
5
5
  EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_MP_LOCATION_CHANGE, EV_POPSTATE,
6
6
  EV_SCROLLEND, EV_SUBMIT
7
7
  } from './utils';
8
+ import { RageClickTracker } from './rageclick';
8
9
 
9
10
  var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
10
11
  var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
@@ -27,6 +28,7 @@ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
27
28
  var CONFIG_TRACK_CLICK = 'click';
28
29
  var CONFIG_TRACK_INPUT = 'input';
29
30
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
31
+ var CONFIG_TRACK_RAGE_CLICK = 'rage_click';
30
32
  var CONFIG_TRACK_SCROLL = 'scroll';
31
33
  var CONFIG_TRACK_SUBMIT = 'submit';
32
34
 
@@ -44,6 +46,7 @@ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
44
46
  CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
45
47
  CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
46
48
  CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
49
+ CONFIG_DEFAULTS[CONFIG_TRACK_RAGE_CLICK] = true;
47
50
  CONFIG_DEFAULTS[CONFIG_TRACK_SCROLL] = true;
48
51
  CONFIG_DEFAULTS[CONFIG_TRACK_SUBMIT] = true;
49
52
 
@@ -53,6 +56,7 @@ var DEFAULT_PROPS = {
53
56
 
54
57
  var MP_EV_CLICK = '$mp_click';
55
58
  var MP_EV_INPUT = '$mp_input_change';
59
+ var MP_EV_RAGE_CLICK = '$mp_rage_click';
56
60
  var MP_EV_SCROLL = '$mp_scroll';
57
61
  var MP_EV_SUBMIT = '$mp_submit';
58
62
 
@@ -75,6 +79,7 @@ Autocapture.prototype.init = function() {
75
79
  this.initInputTracking();
76
80
  this.initScrollTracking();
77
81
  this.initSubmitTracking();
82
+ this.initRageClickTracking();
78
83
  };
79
84
 
80
85
  Autocapture.prototype.getFullConfig = function() {
@@ -153,6 +158,11 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
153
158
  return;
154
159
  }
155
160
 
161
+ var isCapturedForHeatMap = this.mp.is_recording_heatmap_data() && (
162
+ (mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK)) ||
163
+ (mpEventName === MP_EV_RAGE_CLICK && !this._getRageClickConfig())
164
+ );
165
+
156
166
  var props = getPropsForDOMEvent(ev, {
157
167
  allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
158
168
  allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
@@ -161,7 +171,7 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
161
171
  blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
162
172
  captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
163
173
  captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT),
164
- capturedForHeatMap: mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK) && this.mp.is_recording_heatmap_data(),
174
+ capturedForHeatMap: isCapturedForHeatMap,
165
175
  });
166
176
  if (props) {
167
177
  _.extend(props, DEFAULT_PROPS);
@@ -169,6 +179,24 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
169
179
  }
170
180
  };
171
181
 
182
+ Autocapture.prototype._getRageClickConfig = function() {
183
+ var config = this.getConfig(CONFIG_TRACK_RAGE_CLICK);
184
+
185
+ if (!config) {
186
+ return null; // rage click tracking disabled
187
+ }
188
+
189
+ if (config === true) {
190
+ return {}; // use defaults
191
+ }
192
+
193
+ if (typeof config === 'object') {
194
+ return config; // use custom configuration
195
+ }
196
+
197
+ return {}; // fallback to defaults for any other truthy value
198
+ };
199
+
172
200
  Autocapture.prototype.initClickTracking = function() {
173
201
  window.removeEventListener(EV_CLICK, this.listenerClick);
174
202
 
@@ -270,6 +298,36 @@ Autocapture.prototype.initPageviewTracking = function() {
270
298
  }.bind(this)));
271
299
  };
272
300
 
301
+ Autocapture.prototype.initRageClickTracking = function() {
302
+ window.removeEventListener(EV_CLICK, this.listenerRageClick);
303
+
304
+ var rageClickConfig = this._getRageClickConfig();
305
+ if (!rageClickConfig && !this.mp.get_config('record_heatmap_data')) {
306
+ return;
307
+ }
308
+
309
+ logger.log('Initializing rage click tracking');
310
+ if (!this._rageClickTracker) {
311
+ this._rageClickTracker = new RageClickTracker();
312
+ }
313
+
314
+ this.listenerRageClick = function(ev) {
315
+ var currentRageClickConfig = this._getRageClickConfig();
316
+ if (!currentRageClickConfig && !this.mp.is_recording_heatmap_data()) {
317
+ return;
318
+ }
319
+
320
+ if (this.currentUrlBlocked()) {
321
+ return;
322
+ }
323
+
324
+ if (this._rageClickTracker.isRageClick(ev['pageX'], ev['pageY'], currentRageClickConfig)) {
325
+ this.trackDomEvent(ev, MP_EV_RAGE_CLICK);
326
+ }
327
+ }.bind(this);
328
+ window.addEventListener(EV_CLICK, this.listenerRageClick);
329
+ };
330
+
273
331
  Autocapture.prototype.initScrollTracking = function() {
274
332
  window.removeEventListener(EV_SCROLLEND, this.listenerScroll);
275
333
 
@@ -0,0 +1,38 @@
1
+ /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
2
+ /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
3
+ /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
4
+
5
+ function RageClickTracker() {
6
+ this.clicks = [];
7
+ }
8
+
9
+ RageClickTracker.prototype.isRageClick = function(x, y, options) {
10
+ options = options || {};
11
+ var thresholdPx = options['threshold_px'] || DEFAULT_RAGE_CLICK_THRESHOLD_PX;
12
+ var timeoutMs = options['timeout_ms'] || DEFAULT_RAGE_CLICK_TIMEOUT_MS;
13
+ var clickCount = options['click_count'] || DEFAULT_RAGE_CLICK_CLICK_COUNT;
14
+ var timestamp = Date.now();
15
+
16
+ var lastClick = this.clicks[this.clicks.length - 1];
17
+ if (
18
+ lastClick &&
19
+ timestamp - lastClick.timestamp < timeoutMs &&
20
+ Math.sqrt(Math.pow(x - lastClick.x, 2) + Math.pow(y - lastClick.y, 2)) < thresholdPx
21
+ ) {
22
+ this.clicks.push({ x: x, y: y, timestamp: timestamp });
23
+ if (this.clicks.length >= clickCount) {
24
+ this.clicks = [];
25
+ return true;
26
+ }
27
+ } else {
28
+ this.clicks = [{ x: x, y: y, timestamp: timestamp }];
29
+ }
30
+ return false;
31
+ };
32
+
33
+ export {
34
+ RageClickTracker,
35
+ DEFAULT_RAGE_CLICK_THRESHOLD_PX,
36
+ DEFAULT_RAGE_CLICK_TIMEOUT_MS,
37
+ DEFAULT_RAGE_CLICK_CLICK_COUNT
38
+ };
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.66.0'
3
+ LIB_VERSION: '2.68.0'
4
4
  };
5
5
 
6
6
  export default Config;