mixpanel-browser 2.45.0 → 2.47.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.
@@ -57,6 +57,7 @@ var NOOP_FUNC = function() {};
57
57
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
58
58
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
59
59
  /** @const */ var PAYLOAD_TYPE_JSON = 'json';
60
+ /** @const */ var DEVICE_ID_PREFIX = '$device:';
60
61
 
61
62
 
62
63
  /*
@@ -98,6 +99,9 @@ var DEFAULT_CONFIG = {
98
99
  'cookie_domain': '',
99
100
  'cookie_name': '',
100
101
  'loaded': NOOP_FUNC,
102
+ 'track_marketing': true,
103
+ 'track_pageview': false,
104
+ 'skip_first_touch_marketing': false,
101
105
  'store_google': true,
102
106
  'save_referrer': true,
103
107
  'test': false,
@@ -164,6 +168,25 @@ var create_mplib = function(token, config, name) {
164
168
  instance['people'] = new MixpanelPeople();
165
169
  instance['people']._init(instance);
166
170
 
171
+ if (!instance.get_config('skip_first_touch_marketing')) {
172
+ // We need null UTM params in the object because
173
+ // UTM parameters act as a tuple. If any UTM param
174
+ // is present, then we set all UTM params including
175
+ // empty ones together
176
+ var utm_params = _.info.campaignParams(null);
177
+ var initial_utm_params = {};
178
+ var has_utm = false;
179
+ _.each(utm_params, function(utm_value, utm_key) {
180
+ initial_utm_params['initial_' + utm_key] = utm_value;
181
+ if (utm_value) {
182
+ has_utm = true;
183
+ }
184
+ });
185
+ if (has_utm) {
186
+ instance['people'].set_once(initial_utm_params);
187
+ }
188
+ }
189
+
167
190
  // if any instance on the page has debug = true, we set the
168
191
  // global debug to be true
169
192
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
@@ -195,7 +218,7 @@ var create_mplib = function(token, config, name) {
195
218
  * mixpanel.library_name.track(...);
196
219
  *
197
220
  * @param {String} token Your Mixpanel API token
198
- * @param {Object} [config] A dictionary of config options to override. <a href="https://github.com/mixpanel/mixpanel-js/blob/8b2e1f7b/src/mixpanel-core.js#L87-L110">See a list of default config options</a>.
221
+ * @param {Object} [config] A dictionary of config options to override. <a href="https://github.com/mixpanel/mixpanel-js/blob/v2.46.0/src/mixpanel-core.js#L88-L127">See a list of default config options</a>.
199
222
  * @param {String} [name] The name for the new mixpanel instance that you want created
200
223
  */
201
224
  MixpanelLib.prototype.init = function (token, config, name) {
@@ -233,7 +256,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
233
256
  // default to JSON payload for standard mixpanel.com API hosts
234
257
  if (!('api_payload_format' in config)) {
235
258
  var api_host = config['api_host'] || DEFAULT_CONFIG['api_host'];
236
- if (api_host.match(/\.mixpanel\.com$/)) {
259
+ if (api_host.match(/\.mixpanel\.com/)) {
237
260
  variable_features['api_payload_format'] = PAYLOAD_TYPE_JSON;
238
261
  }
239
262
  }
@@ -304,10 +327,14 @@ MixpanelLib.prototype._init = function(token, config, name) {
304
327
  // or the device id if something was already stored
305
328
  // in the persitence
306
329
  this.register_once({
307
- 'distinct_id': uuid,
330
+ 'distinct_id': DEVICE_ID_PREFIX + uuid,
308
331
  '$device_id': uuid
309
332
  }, '');
310
333
  }
334
+
335
+ if (this.get_config('track_pageview')) {
336
+ this.track_pageview();
337
+ }
311
338
  };
312
339
 
313
340
  // Private methods
@@ -321,7 +348,7 @@ MixpanelLib.prototype._loaded = function() {
321
348
  MixpanelLib.prototype._set_default_superprops = function() {
322
349
  this['persistence'].update_search_keyword(document.referrer);
323
350
  if (this.get_config('store_google')) {
324
- this['persistence'].update_campaign_params();
351
+ this.register(_.info.campaignParams(), {persistent: false});
325
352
  }
326
353
  if (this.get_config('save_referrer')) {
327
354
  this['persistence'].update_referrer_info(document.referrer);
@@ -803,6 +830,10 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
803
830
 
804
831
  this._set_default_superprops();
805
832
 
833
+ var marketing_properties = this.get_config('track_marketing')
834
+ ? _.info.marketingParams()
835
+ : {};
836
+
806
837
  // note: extend writes to the first object, so lets make sure we
807
838
  // don't write to the persistence properties object and info
808
839
  // properties object by passing in a new object
@@ -811,6 +842,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
811
842
  properties = _.extend(
812
843
  {},
813
844
  _.info.properties(),
845
+ marketing_properties,
814
846
  this['persistence'].properties(),
815
847
  this.unpersisted_superprops,
816
848
  properties
@@ -971,17 +1003,54 @@ MixpanelLib.prototype.get_group = function (group_key, group_id) {
971
1003
  };
972
1004
 
973
1005
  /**
974
- * Track mp_page_view event. This is now ignored by the server.
1006
+ * Track a default Mixpanel page view event, which includes extra default event properties to
1007
+ * improve page view data. The `config.track_pageview` option for <a href="#mixpanelinit">mixpanel.init()</a>
1008
+ * may be turned on for tracking page loads automatically.
975
1009
  *
976
- * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
977
- * @deprecated
1010
+ * ### Usage
1011
+ *
1012
+ * // track a default $mp_web_page_view event
1013
+ * mixpanel.track_pageview();
1014
+ *
1015
+ * // track a page view event with additional event properties
1016
+ * mixpanel.track_pageview({'ab_test_variant': 'card-layout-b'});
1017
+ *
1018
+ * // example approach to track page views on different page types as event properties
1019
+ * mixpanel.track_pageview({'page': 'pricing'});
1020
+ * mixpanel.track_pageview({'page': 'homepage'});
1021
+ *
1022
+ * // UNCOMMON: Tracking a page view event with a custom event_name option. NOT expected to be used for
1023
+ * // individual pages on the same site or product. Use cases for custom event_name may be page
1024
+ * // views on different products or internal applications that are considered completely separate
1025
+ * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'});
1026
+ *
1027
+ * @param {Object} [properties] An optional set of additional properties to send with the page view event
1028
+ * @param {Object} [options] Page view tracking options
1029
+ * @param {String} [options.event_name] - Alternate name for the tracking event
1030
+ * @returns {Boolean|Object} If the tracking request was successfully initiated/queued, an object
1031
+ * with the tracking payload sent to the API server is returned; otherwise false.
978
1032
  */
979
- MixpanelLib.prototype.track_pageview = function(page) {
980
- if (_.isUndefined(page)) {
981
- page = document.location.href;
1033
+ MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(properties, options) {
1034
+ if (typeof properties !== 'object') {
1035
+ properties = {};
982
1036
  }
983
- this.track('mp_page_view', _.info.pageviewInfo(page));
984
- };
1037
+ options = options || {};
1038
+ var event_name = options['event_name'] || '$mp_web_page_view';
1039
+
1040
+ var default_page_properties = _.extend(
1041
+ _.info.mpPageViewProperties(),
1042
+ _.info.campaignParams(),
1043
+ _.info.clickParams()
1044
+ );
1045
+
1046
+ var event_properties = _.extend(
1047
+ {},
1048
+ default_page_properties,
1049
+ properties
1050
+ );
1051
+
1052
+ return this.track(event_name, event_properties);
1053
+ });
985
1054
 
986
1055
  /**
987
1056
  * Track clicks on a set of document elements. Selector must be a
@@ -1230,7 +1299,15 @@ MixpanelLib.prototype.identify = function(
1230
1299
  // _unset_callback:function A callback to be run if and when the People unset queue is flushed
1231
1300
 
1232
1301
  var previous_distinct_id = this.get_distinct_id();
1233
- this.register({'$user_id': new_distinct_id});
1302
+ if (new_distinct_id && previous_distinct_id !== new_distinct_id) {
1303
+ // we allow the following condition if previous distinct_id is same as new_distinct_id
1304
+ // so that you can force flush people updates for anonymous profiles.
1305
+ if (typeof new_distinct_id === 'string' && new_distinct_id.indexOf(DEVICE_ID_PREFIX) === 0) {
1306
+ this.report_error('distinct_id cannot have $device: prefix');
1307
+ return -1;
1308
+ }
1309
+ this.register({'$user_id': new_distinct_id});
1310
+ }
1234
1311
 
1235
1312
  if (!this.get_property('$device_id')) {
1236
1313
  // The persisted distinct id might not actually be a device id at all
@@ -1271,7 +1348,7 @@ MixpanelLib.prototype.reset = function() {
1271
1348
  this._flags.identify_called = false;
1272
1349
  var uuid = _.UUID();
1273
1350
  this.register_once({
1274
- 'distinct_id': uuid,
1351
+ 'distinct_id': DEVICE_ID_PREFIX + uuid,
1275
1352
  '$device_id': uuid
1276
1353
  }, '');
1277
1354
  };
@@ -1396,8 +1473,8 @@ MixpanelLib.prototype.name_tag = function(name_tag) {
1396
1473
  * // batching or retry mechanisms.
1397
1474
  * api_transport: 'XHR'
1398
1475
  *
1399
- * // turn on request-batching/queueing/retry
1400
- * batch_requests: false,
1476
+ * // request-batching/queueing/retry
1477
+ * batch_requests: true,
1401
1478
  *
1402
1479
  * // maximum number of events/updates to send in a single
1403
1480
  * // network request
@@ -1469,10 +1546,20 @@ MixpanelLib.prototype.name_tag = function(name_tag) {
1469
1546
  * // secure, meaning they will only be transmitted over https
1470
1547
  * secure_cookie: false
1471
1548
  *
1549
+ * // disables enriching user profiles with first touch marketing data
1550
+ * skip_first_touch_marketing: false
1551
+ *
1472
1552
  * // the amount of time track_links will
1473
1553
  * // wait for Mixpanel's servers to respond
1474
1554
  * track_links_timeout: 300
1475
1555
  *
1556
+ * // adds any UTM parameters and click IDs present on the page to any events fired
1557
+ * track_marketing: true
1558
+ *
1559
+ * // enables automatic page view tracking using default page view events through
1560
+ * // the track_pageview() method
1561
+ * track_pageview: false
1562
+ *
1476
1563
  * // if you set upgrade to be true, the library will check for
1477
1564
  * // a cookie from our old js library and import super
1478
1565
  * // properties from it, then the old cookie is deleted
@@ -244,24 +244,25 @@ MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name
244
244
  });
245
245
 
246
246
  /*
247
- * Record that you have charged the current user a certain amount
248
- * of money. Charges recorded with track_charge() will appear in the
249
- * Mixpanel revenue report.
250
- *
251
- * ### Usage:
252
- *
253
- * // charge a user $50
254
- * mixpanel.people.track_charge(50);
255
- *
256
- * // charge a user $30.50 on the 2nd of january
257
- * mixpanel.people.track_charge(30.50, {
258
- * '$time': new Date('jan 1 2012')
259
- * });
260
- *
261
- * @param {Number} amount The amount of money charged to the current user
262
- * @param {Object} [properties] An associative array of properties associated with the charge
263
- * @param {Function} [callback] If provided, the callback will be called when the server responds
264
- */
247
+ * Record that you have charged the current user a certain amount
248
+ * of money. Charges recorded with track_charge() will appear in the
249
+ * Mixpanel revenue report.
250
+ *
251
+ * ### Usage:
252
+ *
253
+ * // charge a user $50
254
+ * mixpanel.people.track_charge(50);
255
+ *
256
+ * // charge a user $30.50 on the 2nd of january
257
+ * mixpanel.people.track_charge(30.50, {
258
+ * '$time': new Date('jan 1 2012')
259
+ * });
260
+ *
261
+ * @param {Number} amount The amount of money charged to the current user
262
+ * @param {Object} [properties] An associative array of properties associated with the charge
263
+ * @param {Function} [callback] If provided, the callback will be called when the server responds
264
+ * @deprecated
265
+ */
265
266
  MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) {
266
267
  if (!_.isNumber(amount)) {
267
268
  amount = parseFloat(amount);
@@ -277,15 +278,16 @@ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(am
277
278
  });
278
279
 
279
280
  /*
280
- * Permanently clear all revenue report transactions from the
281
- * current user's people analytics profile.
282
- *
283
- * ### Usage:
284
- *
285
- * mixpanel.people.clear_charges();
286
- *
287
- * @param {Function} [callback] If provided, the callback will be called after tracking the event.
288
- */
281
+ * Permanently clear all revenue report transactions from the
282
+ * current user's people analytics profile.
283
+ *
284
+ * ### Usage:
285
+ *
286
+ * mixpanel.people.clear_charges();
287
+ *
288
+ * @param {Function} [callback] If provided, the callback will be called after tracking the event.
289
+ * @deprecated
290
+ */
289
291
  MixpanelPeople.prototype.clear_charges = function(callback) {
290
292
  return this.set('$transactions', [], callback);
291
293
  };
@@ -219,13 +219,6 @@ MixpanelPersistence.prototype.unregister = function(prop) {
219
219
  }
220
220
  };
221
221
 
222
- MixpanelPersistence.prototype.update_campaign_params = function() {
223
- if (!this.campaign_params_saved) {
224
- this.register_once(_.info.campaignParams());
225
- this.campaign_params_saved = true;
226
- }
227
- };
228
-
229
222
  MixpanelPersistence.prototype.update_search_keyword = function(referrer) {
230
223
  this.register(_.info.searchInfo(referrer));
231
224
  };
@@ -1,3 +1,4 @@
1
+ import Config from './config';
1
2
  import { RequestQueue } from './request-queue';
2
3
  import { console_with_prefix, _ } from './utils'; // eslint-disable-line camelcase
3
4
 
@@ -30,6 +31,9 @@ var RequestBatcher = function(storageKey, options) {
30
31
 
31
32
  this.stopped = !this.libConfig['batch_autostart'];
32
33
  this.consecutiveRemovalFailures = 0;
34
+
35
+ // extra client-side dedupe
36
+ this.itemIdsSentSuccessfully = {};
33
37
  };
34
38
 
35
39
  /**
@@ -122,7 +126,34 @@ RequestBatcher.prototype.flush = function(options) {
122
126
  payload = this.beforeSendHook(payload);
123
127
  }
124
128
  if (payload) {
125
- dataForRequest.push(payload);
129
+ // mp_sent_by_lib_version prop captures which lib version actually
130
+ // sends each event (regardless of which version originally queued
131
+ // it for sending)
132
+ if (payload['event'] && payload['properties']) {
133
+ payload['properties'] = _.extend(
134
+ {},
135
+ payload['properties'],
136
+ {'mp_sent_by_lib_version': Config.LIB_VERSION}
137
+ );
138
+ }
139
+ var addPayload = true;
140
+ var itemId = item['id'];
141
+ if (itemId) {
142
+ if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
143
+ this.reportError('[dupe] item ID sent too many times, not sending', {
144
+ item: item,
145
+ batchSize: batch.length,
146
+ timesSent: this.itemIdsSentSuccessfully[itemId]
147
+ });
148
+ addPayload = false;
149
+ }
150
+ } else {
151
+ this.reportError('[dupe] found item with no ID', {item: item});
152
+ }
153
+
154
+ if (addPayload) {
155
+ dataForRequest.push(payload);
156
+ }
126
157
  }
127
158
  transformedItems[item['id']] = payload;
128
159
  }, this);
@@ -205,6 +236,24 @@ RequestBatcher.prototype.flush = function(options) {
205
236
  }
206
237
  }, this)
207
238
  );
239
+
240
+ // client-side dedupe
241
+ _.each(batch, _.bind(function(item) {
242
+ var itemId = item['id'];
243
+ if (itemId) {
244
+ this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
245
+ this.itemIdsSentSuccessfully[itemId]++;
246
+ if (this.itemIdsSentSuccessfully[itemId] > 5) {
247
+ this.reportError('[dupe] item ID sent too many times', {
248
+ item: item,
249
+ batchSize: batch.length,
250
+ timesSent: this.itemIdsSentSuccessfully[itemId]
251
+ });
252
+ }
253
+ } else {
254
+ this.reportError('[dupe] found item with no ID while removing', {item: item});
255
+ }
256
+ }, this));
208
257
  }
209
258
 
210
259
  } catch(err) {
package/src/utils.js CHANGED
@@ -830,20 +830,24 @@ _.utf8Encode = function(string) {
830
830
 
831
831
  _.UUID = (function() {
832
832
 
833
- // Time/ticks information
834
- // 1*new Date() is a cross browser version of Date.now()
833
+ // Time-based entropy
835
834
  var T = function() {
836
- var d = 1 * new Date(),
837
- i = 0;
838
-
839
- // this while loop figures how many browser ticks go by
840
- // before 1*new Date() returns a new number, ie the amount
841
- // of ticks that go by per millisecond
842
- while (d == 1 * new Date()) {
843
- i++;
835
+ var time = 1 * new Date(); // cross-browser version of Date.now()
836
+ var ticks;
837
+ if (win.performance && win.performance.now) {
838
+ ticks = win.performance.now();
839
+ } else {
840
+ // fall back to busy loop
841
+ ticks = 0;
842
+
843
+ // this while loop figures how many browser ticks go by
844
+ // before 1*new Date() returns a new number, ie the amount
845
+ // of ticks that go by per millisecond
846
+ while (time == 1 * new Date()) {
847
+ ticks++;
848
+ }
844
849
  }
845
-
846
- return d.toString(16) + i.toString(16);
850
+ return time.toString(16) + Math.floor(ticks).toString(16);
847
851
  };
848
852
 
849
853
  // Math.Random entropy
@@ -1410,21 +1414,42 @@ _.dom_query = (function() {
1410
1414
  };
1411
1415
  })();
1412
1416
 
1417
+ var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
1418
+ var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
1419
+
1413
1420
  _.info = {
1414
- campaignParams: function() {
1415
- var campaign_keywords = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' '),
1416
- kw = '',
1421
+ campaignParams: function(default_value) {
1422
+ var kw = '',
1417
1423
  params = {};
1418
- _.each(campaign_keywords, function(kwkey) {
1424
+ _.each(CAMPAIGN_KEYWORDS, function(kwkey) {
1419
1425
  kw = _.getQueryParam(document.URL, kwkey);
1420
1426
  if (kw.length) {
1421
1427
  params[kwkey] = kw;
1428
+ } else if (default_value !== undefined) {
1429
+ params[kwkey] = default_value;
1422
1430
  }
1423
1431
  });
1424
1432
 
1425
1433
  return params;
1426
1434
  },
1427
1435
 
1436
+ clickParams: function() {
1437
+ var id = '',
1438
+ params = {};
1439
+ _.each(CLICK_IDS, function(idkey) {
1440
+ id = _.getQueryParam(document.URL, idkey);
1441
+ if (id.length) {
1442
+ params[idkey] = id;
1443
+ }
1444
+ });
1445
+
1446
+ return params;
1447
+ },
1448
+
1449
+ marketingParams: function() {
1450
+ return _.extend(_.info.campaignParams(), _.info.clickParams());
1451
+ },
1452
+
1428
1453
  searchEngine: function(referrer) {
1429
1454
  if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
1430
1455
  return 'google';
@@ -1621,12 +1646,13 @@ _.info = {
1621
1646
  });
1622
1647
  },
1623
1648
 
1624
- pageviewInfo: function(page) {
1649
+ mpPageViewProperties: function() {
1625
1650
  return _.strip_empty_properties({
1626
- 'mp_page': page,
1627
- 'mp_referrer': document.referrer,
1628
- 'mp_browser': _.info.browser(userAgent, navigator.vendor, windowOpera),
1629
- 'mp_platform': _.info.os()
1651
+ 'current_page_title': document.title,
1652
+ 'current_domain': win.location.hostname,
1653
+ 'current_url_path': win.location.pathname,
1654
+ 'current_url_protocol': win.location.protocol,
1655
+ 'current_url_search': win.location.search
1630
1656
  });
1631
1657
  }
1632
1658
  };