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.
@@ -15,8 +15,10 @@ CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
15
15
  * @constructor
16
16
  */
17
17
  var FeatureFlagManager = function(initOptions) {
18
+ this.getFullApiRoute = initOptions.getFullApiRoute;
18
19
  this.getMpConfig = initOptions.getConfigFunc;
19
- this.getDistinctId = initOptions.getDistinctIdFunc;
20
+ this.setMpConfig = initOptions.setConfigFunc;
21
+ this.getMpProperty = initOptions.getPropertyFunc;
20
22
  this.track = initOptions.trackingFunc;
21
23
  };
22
24
 
@@ -53,6 +55,23 @@ FeatureFlagManager.prototype.isSystemEnabled = function() {
53
55
  return !!this.getMpConfig(FLAGS_CONFIG_KEY);
54
56
  };
55
57
 
58
+ FeatureFlagManager.prototype.updateContext = function(newContext, options) {
59
+ if (!this.isSystemEnabled()) {
60
+ logger.critical('Feature Flags not enabled, cannot update context');
61
+ return Promise.resolve();
62
+ }
63
+
64
+ var ffConfig = this.getMpConfig(FLAGS_CONFIG_KEY);
65
+ if (!_.isObject(ffConfig)) {
66
+ ffConfig = {};
67
+ }
68
+ var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
69
+ ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
70
+
71
+ this.setMpConfig(FLAGS_CONFIG_KEY, ffConfig);
72
+ return this.fetchFlags();
73
+ };
74
+
56
75
  FeatureFlagManager.prototype.areFlagsReady = function() {
57
76
  if (!this.isSystemEnabled()) {
58
77
  logger.error('Feature Flags not enabled');
@@ -62,15 +81,17 @@ FeatureFlagManager.prototype.areFlagsReady = function() {
62
81
 
63
82
  FeatureFlagManager.prototype.fetchFlags = function() {
64
83
  if (!this.isSystemEnabled()) {
65
- return;
84
+ return Promise.resolve();
66
85
  }
67
86
 
68
- var distinctId = this.getDistinctId();
87
+ var distinctId = this.getMpProperty('distinct_id');
88
+ var deviceId = this.getMpProperty('$device_id');
69
89
  logger.log('Fetching flags for distinct ID: ' + distinctId);
70
90
  var reqParams = {
71
- 'context': _.extend({'distinct_id': distinctId}, this.getConfig(CONFIG_CONTEXT))
91
+ 'context': _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT))
72
92
  };
73
- this.fetchPromise = window['fetch'](this.getMpConfig('api_host') + '/' + this.getMpConfig('api_routes')['flags'], {
93
+ this._fetchInProgressStartTime = Date.now();
94
+ this.fetchPromise = window['fetch'](this.getFullApiRoute(), {
74
95
  'method': 'POST',
75
96
  'headers': {
76
97
  'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
@@ -78,6 +99,7 @@ FeatureFlagManager.prototype.fetchFlags = function() {
78
99
  },
79
100
  'body': JSON.stringify(reqParams)
80
101
  }).then(function(response) {
102
+ this.markFetchComplete();
81
103
  return response.json().then(function(responseBody) {
82
104
  var responseFlags = responseBody['flags'];
83
105
  if (!responseFlags) {
@@ -92,9 +114,26 @@ FeatureFlagManager.prototype.fetchFlags = function() {
92
114
  });
93
115
  this.flags = flags;
94
116
  }.bind(this)).catch(function(error) {
117
+ this.markFetchComplete();
95
118
  logger.error(error);
96
- });
97
- }.bind(this)).catch(function() {});
119
+ }.bind(this));
120
+ }.bind(this)).catch(function(error) {
121
+ this.markFetchComplete();
122
+ logger.error(error);
123
+ }.bind(this));
124
+
125
+ return this.fetchPromise;
126
+ };
127
+
128
+ FeatureFlagManager.prototype.markFetchComplete = function() {
129
+ if (!this._fetchInProgressStartTime) {
130
+ logger.error('Fetch in progress started time not set, cannot mark fetch complete');
131
+ return;
132
+ }
133
+ this._fetchStartTime = this._fetchInProgressStartTime;
134
+ this._fetchCompleteTime = Date.now();
135
+ this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
136
+ this._fetchInProgressStartTime = null;
98
137
  };
99
138
 
100
139
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
@@ -173,7 +212,10 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
173
212
  this.track('$experiment_started', {
174
213
  'Experiment name': featureName,
175
214
  'Variant name': feature['key'],
176
- '$experiment_type': 'feature_flag'
215
+ '$experiment_type': 'feature_flag',
216
+ 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
217
+ 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
218
+ 'Variant fetch latency (ms)': this._fetchLatency
177
219
  });
178
220
  };
179
221
 
@@ -193,6 +235,7 @@ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype
193
235
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
194
236
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
195
237
  FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
238
+ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
196
239
 
197
240
  // Deprecated method
198
241
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
package/src/index.d.ts CHANGED
@@ -40,6 +40,17 @@ export interface OutTrackingOptions extends ClearOptOutInOutOptions {
40
40
  delete_user: boolean;
41
41
  }
42
42
 
43
+ export type RageClickConfig =
44
+ | boolean
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
+ };
53
+
43
54
  export interface RegisterOptions {
44
55
  persistent: boolean;
45
56
  }
@@ -66,6 +77,12 @@ export interface AutocaptureConfig {
66
77
  * @default 'full-url'
67
78
  */
68
79
  pageview?: TrackPageView;
80
+ /**
81
+ * When set to `true`, Mixpanel will track rage clicks (multiple clicks in a short time on the same element).
82
+ * Can also be configured as an object to customize rage click detection parameters.
83
+ * @default true
84
+ */
85
+ rage_click?: RageClickConfig;
69
86
  /**
70
87
  * When set, Mixpanel will collect page scrolls at specified scroll intervals.
71
88
  * @default true
@@ -184,6 +201,7 @@ export interface Config {
184
201
  record_max_ms: number;
185
202
  record_sessions_percent: number;
186
203
  record_canvas: boolean;
204
+ record_heatmap_data: boolean;
187
205
  }
188
206
 
189
207
  export type VerboseResponse =
@@ -149,7 +149,7 @@ var DEFAULT_CONFIG = {
149
149
  'batch_autostart': true,
150
150
  'hooks': {},
151
151
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
152
- 'record_block_selector': 'img, video',
152
+ 'record_block_selector': 'img, video, audio',
153
153
  'record_canvas': false,
154
154
  'record_collect_fonts': false,
155
155
  'record_heatmap_data': false,
@@ -369,8 +369,12 @@ MixpanelLib.prototype._init = function(token, config, name) {
369
369
  }
370
370
 
371
371
  this.flags = new FeatureFlagManager({
372
+ getFullApiRoute: _.bind(function() {
373
+ return this.get_api_host('flags') + '/' + this.get_config('api_routes')['flags'];
374
+ }, this),
372
375
  getConfigFunc: _.bind(this.get_config, this),
373
- getDistinctIdFunc: _.bind(this.get_distinct_id, this),
376
+ setConfigFunc: _.bind(this.set_config, this),
377
+ getPropertyFunc: _.bind(this.get_property, this),
374
378
  trackingFunc: _.bind(this.track, this)
375
379
  });
376
380
  this.flags.init();
@@ -856,11 +860,10 @@ MixpanelLib.prototype.are_batchers_initialized = function() {
856
860
 
857
861
  MixpanelLib.prototype.get_batcher_configs = function() {
858
862
  var queue_prefix = '__mpq_' + this.get_config('token');
859
- var api_routes = this.get_config('api_routes');
860
863
  this._batcher_configs = this._batcher_configs || {
861
- events: {type: 'events', endpoint: '/' + api_routes['track'], queue_key: queue_prefix + '_ev'},
862
- people: {type: 'people', endpoint: '/' + api_routes['engage'], queue_key: queue_prefix + '_pp'},
863
- groups: {type: 'groups', endpoint: '/' + api_routes['groups'], queue_key: queue_prefix + '_gr'}
864
+ events: {type: 'events', api_name: 'track', queue_key: queue_prefix + '_ev'},
865
+ people: {type: 'people', api_name: 'engage', queue_key: queue_prefix + '_pp'},
866
+ groups: {type: 'groups', api_name: 'groups', queue_key: queue_prefix + '_gr'}
864
867
  };
865
868
  return this._batcher_configs;
866
869
  };
@@ -874,8 +877,9 @@ MixpanelLib.prototype.init_batchers = function() {
874
877
  libConfig: this['config'],
875
878
  errorReporter: this.get_config('error_reporter'),
876
879
  sendRequestFunc: _.bind(function(data, options, cb) {
880
+ var api_routes = this.get_config('api_routes');
877
881
  this._send_request(
878
- this.get_config('api_host') + attrs.endpoint,
882
+ this.get_api_host(attrs.api_name) + '/' + api_routes[attrs.api_name],
879
883
  this._encode_data_for_request(data),
880
884
  options,
881
885
  this._prepare_callback(cb, data)
@@ -1603,31 +1607,15 @@ MixpanelLib.prototype.identify = function(
1603
1607
  * Useful for clearing data when a user logs out.
1604
1608
  */
1605
1609
  MixpanelLib.prototype.reset = function() {
1606
- var self = this;
1607
-
1608
- var reset = function () {
1609
- self['persistence'].clear();
1610
- self._flags.identify_called = false;
1611
- var uuid = _.UUID();
1612
- self.register_once({
1613
- 'distinct_id': DEVICE_ID_PREFIX + uuid,
1614
- '$device_id': uuid
1615
- }, '');
1616
- };
1617
-
1618
- if (self._recorder) {
1619
- self.stop_session_recording()
1620
- .then(function () {
1621
- reset();
1622
- self._check_and_start_session_recording();
1623
- })
1624
- .catch(_.bind(function (err) {
1625
- reset();
1626
- this.report_error('Error restarting recording session', err);
1627
- }, this));
1628
- } else {
1629
- reset();
1630
- }
1610
+ this.stop_session_recording();
1611
+ this['persistence'].clear();
1612
+ this._flags.identify_called = false;
1613
+ var uuid = _.UUID();
1614
+ this.register_once({
1615
+ 'distinct_id': DEVICE_ID_PREFIX + uuid,
1616
+ '$device_id': uuid
1617
+ }, '');
1618
+ this._check_and_start_session_recording();
1631
1619
  };
1632
1620
 
1633
1621
  /**
@@ -63,6 +63,13 @@ function isUserEvent(ev) {
63
63
  * @property {string} replayStartUrl
64
64
  */
65
65
 
66
+ /**
67
+ * @typedef {Object} UserIdInfo
68
+ * @property {string} distinct_id
69
+ * @property {string} user_id
70
+ * @property {string} device_id
71
+ */
72
+
66
73
 
67
74
  /**
68
75
  * This class encapsulates a single session recording and its lifecycle.
@@ -118,6 +125,30 @@ var SessionRecording = function(options) {
118
125
  });
119
126
  };
120
127
 
128
+ /**
129
+ * @returns {UserIdInfo}
130
+ */
131
+ SessionRecording.prototype.getUserIdInfo = function () {
132
+ if (this.finalFlushUserIdInfo) {
133
+ return this.finalFlushUserIdInfo;
134
+ }
135
+
136
+ var userIdInfo = {
137
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
138
+ };
139
+
140
+ // send ID management props if they exist
141
+ var deviceId = this._mixpanel.get_property('$device_id');
142
+ if (deviceId) {
143
+ userIdInfo['$device_id'] = deviceId;
144
+ }
145
+ var userId = this._mixpanel.get_property('$user_id');
146
+ if (userId) {
147
+ userIdInfo['$user_id'] = userId;
148
+ }
149
+ return userIdInfo;
150
+ };
151
+
121
152
  SessionRecording.prototype.unloadPersistedData = function () {
122
153
  this.batcher.stop();
123
154
  return this.batcher.flush()
@@ -242,6 +273,9 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
242
273
  };
243
274
 
244
275
  SessionRecording.prototype.stopRecording = function (skipFlush) {
276
+ // store the user ID info in case this is getting called in mixpanel.reset()
277
+ this.finalFlushUserIdInfo = this.getUserIdInfo();
278
+
245
279
  if (!this.isRrwebStopped()) {
246
280
  try {
247
281
  this._stopRecording();
@@ -407,7 +441,6 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
407
441
  '$current_url': this.batchStartUrl,
408
442
  '$lib_version': Config.LIB_VERSION,
409
443
  'batch_start_time': batchStartTime / 1000,
410
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
411
444
  'mp_lib': 'web',
412
445
  'replay_id': replayId,
413
446
  'replay_length_ms': replayLengthMs,
@@ -416,16 +449,7 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
416
449
  'seq': this.seqNo
417
450
  };
418
451
  var eventsJson = JSON.stringify(data);
419
-
420
- // send ID management props if they exist
421
- var deviceId = this._mixpanel.get_property('$device_id');
422
- if (deviceId) {
423
- reqParams['$device_id'] = deviceId;
424
- }
425
- var userId = this._mixpanel.get_property('$user_id');
426
- if (userId) {
427
- reqParams['$user_id'] = userId;
428
- }
452
+ Object.assign(reqParams, this.getUserIdInfo());
429
453
 
430
454
  if (CompressionStream) {
431
455
  var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();