mixpanel-browser 2.63.0 → 2.65.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.63.0",
3
+ "version": "2.65.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",
@@ -160,7 +160,8 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
160
160
  blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
161
161
  blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
162
162
  captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
163
- captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
163
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT),
164
+ capturedForHeatMap: mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK) && this.mp.is_recording_heatmap_data(),
164
165
  });
165
166
  if (props) {
166
167
  _.extend(props, DEFAULT_PROPS);
@@ -171,13 +172,13 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
171
172
  Autocapture.prototype.initClickTracking = function() {
172
173
  window.removeEventListener(EV_CLICK, this.listenerClick);
173
174
 
174
- if (!this.getConfig(CONFIG_TRACK_CLICK)) {
175
+ if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
175
176
  return;
176
177
  }
177
178
  logger.log('Initializing click tracking');
178
179
 
179
180
  this.listenerClick = window.addEventListener(EV_CLICK, function(ev) {
180
- if (!this.getConfig(CONFIG_TRACK_CLICK)) {
181
+ if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
181
182
  return;
182
183
  }
183
184
  this.trackDomEvent(ev, MP_EV_CLICK);
@@ -114,6 +114,7 @@ function getPropsForDOMEvent(ev, config) {
114
114
  var blockSelectors = config.blockSelectors || [];
115
115
  var captureTextContent = config.captureTextContent || false;
116
116
  var captureExtraAttrs = config.captureExtraAttrs || [];
117
+ var capturedForHeatMap = config.capturedForHeatMap || false;
117
118
 
118
119
  // convert array to set every time, as the config may have changed
119
120
  var blockAttrsSet = {};
@@ -168,7 +169,9 @@ function getPropsForDOMEvent(ev, config) {
168
169
  '$elements': elementsJson,
169
170
  '$el_attr__href': href,
170
171
  '$viewportHeight': Math.max(docElement['clientHeight'], window['innerHeight'] || 0),
171
- '$viewportWidth': Math.max(docElement['clientWidth'], window['innerWidth'] || 0)
172
+ '$viewportWidth': Math.max(docElement['clientWidth'], window['innerWidth'] || 0),
173
+ '$pageHeight': document['body']['offsetHeight'] || 0,
174
+ '$pageWidth': document['body']['offsetWidth'] || 0,
172
175
  };
173
176
  _.each(captureExtraAttrs, function(attr) {
174
177
  if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
@@ -192,6 +195,9 @@ function getPropsForDOMEvent(ev, config) {
192
195
  props['$' + prop] = ev[prop];
193
196
  }
194
197
  });
198
+ if (capturedForHeatMap) {
199
+ props['$captured_for_heatmap'] = true;
200
+ }
195
201
  target = guessRealClickTarget(ev);
196
202
  }
197
203
  // prioritize text content from "real" click target if different from original target
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.63.0'
3
+ LIB_VERSION: '2.65.0'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -0,0 +1,200 @@
1
+ import { _, console_with_prefix, safewrapClass } from '../utils'; // eslint-disable-line camelcase
2
+ import { window } from '../window';
3
+
4
+ var fetch = window['fetch'];
5
+ var logger = console_with_prefix('flags');
6
+
7
+ var FLAGS_CONFIG_KEY = 'flags';
8
+
9
+ var CONFIG_CONTEXT = 'context';
10
+ var CONFIG_DEFAULTS = {};
11
+ CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
12
+
13
+ /**
14
+ * FeatureFlagManager: support for Mixpanel's feature flagging product
15
+ * @constructor
16
+ */
17
+ var FeatureFlagManager = function(initOptions) {
18
+ this.getMpConfig = initOptions.getConfigFunc;
19
+ this.getDistinctId = initOptions.getDistinctIdFunc;
20
+ this.track = initOptions.trackingFunc;
21
+ };
22
+
23
+ FeatureFlagManager.prototype.init = function() {
24
+ if (!minApisSupported()) {
25
+ logger.critical('Feature Flags unavailable: missing minimum required APIs');
26
+ return;
27
+ }
28
+
29
+ this.flags = null;
30
+ this.fetchFlags();
31
+
32
+ this.trackedFeatures = new Set();
33
+ };
34
+
35
+ FeatureFlagManager.prototype.getFullConfig = function() {
36
+ var ffConfig = this.getMpConfig(FLAGS_CONFIG_KEY);
37
+ if (!ffConfig) {
38
+ // flags are completely off
39
+ return {};
40
+ } else if (_.isObject(ffConfig)) {
41
+ return _.extend({}, CONFIG_DEFAULTS, ffConfig);
42
+ } else {
43
+ // config is non-object truthy value, return default
44
+ return CONFIG_DEFAULTS;
45
+ }
46
+ };
47
+
48
+ FeatureFlagManager.prototype.getConfig = function(key) {
49
+ return this.getFullConfig()[key];
50
+ };
51
+
52
+ FeatureFlagManager.prototype.isSystemEnabled = function() {
53
+ return !!this.getMpConfig(FLAGS_CONFIG_KEY);
54
+ };
55
+
56
+ FeatureFlagManager.prototype.areFlagsReady = function() {
57
+ if (!this.isSystemEnabled()) {
58
+ logger.error('Feature Flags not enabled');
59
+ }
60
+ return !!this.flags;
61
+ };
62
+
63
+ FeatureFlagManager.prototype.fetchFlags = function() {
64
+ if (!this.isSystemEnabled()) {
65
+ return;
66
+ }
67
+
68
+ var distinctId = this.getDistinctId();
69
+ logger.log('Fetching flags for distinct ID: ' + distinctId);
70
+ var reqParams = {
71
+ 'context': _.extend({'distinct_id': distinctId}, this.getConfig(CONFIG_CONTEXT))
72
+ };
73
+ this.fetchPromise = window['fetch'](this.getMpConfig('api_host') + '/' + this.getMpConfig('api_routes')['flags'], {
74
+ 'method': 'POST',
75
+ 'headers': {
76
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
77
+ 'Content-Type': 'application/octet-stream'
78
+ },
79
+ 'body': JSON.stringify(reqParams)
80
+ }).then(function(response) {
81
+ return response.json().then(function(responseBody) {
82
+ var responseFlags = responseBody['flags'];
83
+ if (!responseFlags) {
84
+ throw new Error('No flags in API response');
85
+ }
86
+ var flags = new Map();
87
+ _.each(responseFlags, function(data, key) {
88
+ flags.set(key, {
89
+ 'key': data['variant_key'],
90
+ 'value': data['variant_value']
91
+ });
92
+ });
93
+ this.flags = flags;
94
+ }.bind(this)).catch(function(error) {
95
+ logger.error(error);
96
+ });
97
+ }.bind(this)).catch(function() {});
98
+ };
99
+
100
+ FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
101
+ if (!this.fetchPromise) {
102
+ return new Promise(function(resolve) {
103
+ logger.critical('Feature Flags not initialized');
104
+ resolve(fallback);
105
+ });
106
+ }
107
+
108
+ return this.fetchPromise.then(function() {
109
+ return this.getVariantSync(featureName, fallback);
110
+ }.bind(this)).catch(function(error) {
111
+ logger.error(error);
112
+ return fallback;
113
+ });
114
+ };
115
+
116
+ FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
117
+ if (!this.areFlagsReady()) {
118
+ logger.log('Flags not loaded yet');
119
+ return fallback;
120
+ }
121
+ var feature = this.flags.get(featureName);
122
+ if (!feature) {
123
+ logger.log('No flag found: "' + featureName + '"');
124
+ return fallback;
125
+ }
126
+ this.trackFeatureCheck(featureName, feature);
127
+ return feature;
128
+ };
129
+
130
+ FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
131
+ return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
132
+ return feature['value'];
133
+ }).catch(function(error) {
134
+ logger.error(error);
135
+ return fallbackValue;
136
+ });
137
+ };
138
+
139
+ // TODO remove deprecated method
140
+ FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
141
+ logger.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
142
+ return this.getVariantValue(featureName, fallbackValue);
143
+ };
144
+
145
+ FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
146
+ return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
147
+ };
148
+
149
+ FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
150
+ return this.getVariantValue(featureName).then(function() {
151
+ return this.isEnabledSync(featureName, fallbackValue);
152
+ }.bind(this)).catch(function(error) {
153
+ logger.error(error);
154
+ return fallbackValue;
155
+ });
156
+ };
157
+
158
+ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
159
+ fallbackValue = fallbackValue || false;
160
+ var val = this.getVariantValueSync(featureName, fallbackValue);
161
+ if (val !== true && val !== false) {
162
+ logger.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
163
+ val = fallbackValue;
164
+ }
165
+ return val;
166
+ };
167
+
168
+ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
169
+ if (this.trackedFeatures.has(featureName)) {
170
+ return;
171
+ }
172
+ this.trackedFeatures.add(featureName);
173
+ this.track('$experiment_started', {
174
+ 'Experiment name': featureName,
175
+ 'Variant name': feature['key'],
176
+ '$experiment_type': 'feature_flag'
177
+ });
178
+ };
179
+
180
+ function minApisSupported() {
181
+ return !!fetch &&
182
+ typeof Promise !== 'undefined' &&
183
+ typeof Map !== 'undefined' &&
184
+ typeof Set !== 'undefined';
185
+ }
186
+
187
+ safewrapClass(FeatureFlagManager);
188
+
189
+ FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
190
+ FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
191
+ FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
192
+ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
193
+ FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
194
+ FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
195
+ FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
196
+
197
+ // Deprecated method
198
+ FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
199
+
200
+ export { FeatureFlagManager };
@@ -4,6 +4,7 @@ import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice, NO
4
4
  import { isRecordingExpired } from './recorder/utils';
5
5
  import { window } from './window';
6
6
  import { Autocapture } from './autocapture';
7
+ import { FeatureFlagManager } from './flags';
7
8
  import { FormTracker, LinkTracker } from './dom-trackers';
8
9
  import { RequestBatcher } from './request-batcher';
9
10
  import { MixpanelGroup } from './mixpanel-group';
@@ -86,10 +87,11 @@ if (navigator['sendBeacon']) {
86
87
  }
87
88
 
88
89
  var DEFAULT_API_ROUTES = {
89
- 'track': 'track/',
90
+ 'track': 'track/',
90
91
  'engage': 'engage/',
91
92
  'groups': 'groups/',
92
- 'record': 'record/'
93
+ 'record': 'record/',
94
+ 'flags': 'flags/'
93
95
  };
94
96
 
95
97
  /*
@@ -98,6 +100,7 @@ var DEFAULT_API_ROUTES = {
98
100
  var DEFAULT_CONFIG = {
99
101
  'api_host': 'https://api-js.mixpanel.com',
100
102
  'api_routes': DEFAULT_API_ROUTES,
103
+ 'api_extra_query_params': {},
101
104
  'api_method': 'POST',
102
105
  'api_transport': 'XHR',
103
106
  'api_payload_format': PAYLOAD_TYPE_BASE64,
@@ -107,6 +110,7 @@ var DEFAULT_CONFIG = {
107
110
  'cross_site_cookie': false,
108
111
  'cross_subdomain_cookie': true,
109
112
  'error_reporter': NOOP_FUNC,
113
+ 'flags': false,
110
114
  'persistence': 'cookie',
111
115
  'persistence_name': '',
112
116
  'cookie_domain': '',
@@ -147,6 +151,7 @@ var DEFAULT_CONFIG = {
147
151
  'record_block_selector': 'img, video',
148
152
  'record_canvas': false,
149
153
  'record_collect_fonts': false,
154
+ 'record_heatmap_data': false,
150
155
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
151
156
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
152
157
  'record_mask_text_selector': '*',
@@ -362,6 +367,14 @@ MixpanelLib.prototype._init = function(token, config, name) {
362
367
  }, '');
363
368
  }
364
369
 
370
+ this.flags = new FeatureFlagManager({
371
+ getConfigFunc: _.bind(this.get_config, this),
372
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
373
+ trackingFunc: _.bind(this.track, this)
374
+ });
375
+ this.flags.init();
376
+ this['flags'] = this.flags;
377
+
365
378
  this.autocapture = new Autocapture(this);
366
379
  this.autocapture.init();
367
380
 
@@ -487,6 +500,10 @@ MixpanelLib.prototype.resume_session_recording = function () {
487
500
  }
488
501
  };
489
502
 
503
+ MixpanelLib.prototype.is_recording_heatmap_data = function () {
504
+ return this._get_session_replay_id() && this.get_config('record_heatmap_data');
505
+ };
506
+
490
507
  MixpanelLib.prototype.get_session_recording_properties = function () {
491
508
  var props = {};
492
509
  var replay_id = this._get_session_replay_id();
@@ -671,6 +688,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
671
688
  delete data['data'];
672
689
  }
673
690
 
691
+ _.extend(data, this.get_config('api_extra_query_params'));
692
+
674
693
  url += '?' + _.HTTPBuildQuery(data);
675
694
 
676
695
  var lib = this;
@@ -1568,6 +1587,11 @@ MixpanelLib.prototype.identify = function(
1568
1587
  '$anon_distinct_id': previous_distinct_id
1569
1588
  }, {skip_hooks: true});
1570
1589
  }
1590
+
1591
+ // check feature flags again if distinct id has changed
1592
+ if (new_distinct_id !== previous_distinct_id) {
1593
+ this.flags.fetchFlags();
1594
+ }
1571
1595
  };
1572
1596
 
1573
1597
  /**
@@ -1582,6 +1606,8 @@ MixpanelLib.prototype.reset = function() {
1582
1606
  'distinct_id': DEVICE_ID_PREFIX + uuid,
1583
1607
  '$device_id': uuid
1584
1608
  }, '');
1609
+ this.stop_session_recording();
1610
+ this._check_and_start_session_recording();
1585
1611
  };
1586
1612
 
1587
1613
  /**
@@ -1842,7 +1868,7 @@ MixpanelLib.prototype.set_config = function(config) {
1842
1868
  }
1843
1869
  Config.DEBUG = Config.DEBUG || this.get_config('debug');
1844
1870
 
1845
- if ('autocapture' in config && this.autocapture) {
1871
+ if (('autocapture' in config || 'record_heatmap_data' in config) && this.autocapture) {
1846
1872
  this.autocapture.init();
1847
1873
  }
1848
1874
  }
@@ -262,18 +262,8 @@ MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name
262
262
  * @param {Function} [callback] If provided, the callback will be called when the server responds
263
263
  * @deprecated
264
264
  */
265
- MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) {
266
- if (!_.isNumber(amount)) {
267
- amount = parseFloat(amount);
268
- if (isNaN(amount)) {
269
- console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
270
- return;
271
- }
272
- }
273
-
274
- return this.append('$transactions', _.extend({
275
- '$amount': amount
276
- }, properties), callback);
265
+ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function() {
266
+ console.error('mixpanel.people.track_charge() is deprecated and no longer has any effect.');
277
267
  });
278
268
 
279
269
  /*
package/src/utils.js CHANGED
@@ -1483,6 +1483,9 @@ _.info = {
1483
1483
  return 'Microsoft Edge';
1484
1484
  } else if (_.includes(user_agent, 'FBIOS')) {
1485
1485
  return 'Facebook Mobile';
1486
+ } else if (_.includes(user_agent, 'Whale/')) {
1487
+ // https://user-agents.net/browsers/whale-browser
1488
+ return 'Whale Browser';
1486
1489
  } else if (_.includes(user_agent, 'Chrome')) {
1487
1490
  return 'Chrome';
1488
1491
  } else if (_.includes(user_agent, 'CriOS')) {
@@ -1534,7 +1537,8 @@ _.info = {
1534
1537
  'Android Mobile': /android\s(\d+(\.\d+)?)/,
1535
1538
  'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/,
1536
1539
  'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/,
1537
- 'Mozilla': /rv:(\d+(\.\d+)?)/
1540
+ 'Mozilla': /rv:(\d+(\.\d+)?)/,
1541
+ 'Whale Browser': /Whale\/(\d+(\.\d+)?)/
1538
1542
  };
1539
1543
  var regex = versionRegexs[browser];
1540
1544
  if (regex === undefined) {