mixpanel-browser 2.48.1 → 2.50.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.
@@ -1,6 +1,6 @@
1
1
  /* eslint camelcase: "off" */
2
2
  import Config from './config';
3
- import { _, console, userAgent, window, document, navigator, slice } from './utils';
3
+ import { MAX_RECORDING_MS, _, console, userAgent, window, document, navigator, slice } from './utils';
4
4
  import { FormTracker, LinkTracker } from './dom-trackers';
5
5
  import { RequestBatcher } from './request-batcher';
6
6
  import { MixpanelGroup } from './mixpanel-group';
@@ -84,7 +84,8 @@ if (navigator['sendBeacon']) {
84
84
  var DEFAULT_API_ROUTES = {
85
85
  'track': 'track/',
86
86
  'engage': 'engage/',
87
- 'groups': 'groups/'
87
+ 'groups': 'groups/',
88
+ 'record': 'record/'
88
89
  };
89
90
 
90
91
  /*
@@ -106,10 +107,12 @@ var DEFAULT_CONFIG = {
106
107
  'cookie_domain': '',
107
108
  'cookie_name': '',
108
109
  'loaded': NOOP_FUNC,
110
+ 'mp_loader': null,
109
111
  'track_marketing': true,
110
112
  'track_pageview': false,
111
113
  'skip_first_touch_marketing': false,
112
114
  'store_google': true,
115
+ 'stop_utm_persistence': false,
113
116
  'save_referrer': true,
114
117
  'test': false,
115
118
  'verbose': false,
@@ -134,7 +137,12 @@ var DEFAULT_CONFIG = {
134
137
  'batch_flush_interval_ms': 5000,
135
138
  'batch_request_timeout_ms': 90000,
136
139
  'batch_autostart': true,
137
- 'hooks': {}
140
+ 'hooks': {},
141
+ 'record_sessions_percent': 0,
142
+ 'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
143
+ 'record_max_ms': MAX_RECORDING_MS,
144
+ 'record_mask_text_selector': '*',
145
+ 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
138
146
  };
139
147
 
140
148
  var DOM_LOADED = false;
@@ -343,8 +351,44 @@ MixpanelLib.prototype._init = function(token, config, name) {
343
351
  }, '');
344
352
  }
345
353
 
346
- if (this.get_config('track_pageview')) {
347
- this.track_pageview();
354
+ var track_pageview_option = this.get_config('track_pageview');
355
+ if (track_pageview_option) {
356
+ this._init_url_change_tracking(track_pageview_option);
357
+ }
358
+
359
+ if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
360
+ this.start_session_recording();
361
+ }
362
+ };
363
+
364
+ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () {
365
+ if (!window['MutationObserver']) {
366
+ console.critical('Browser does not support MutationObserver; skipping session recording');
367
+ return;
368
+ }
369
+
370
+ var handleLoadedRecorder = _.bind(function() {
371
+ this._recorder = this._recorder || new window['__mp_recorder'](this);
372
+ this._recorder['startRecording']();
373
+ }, this);
374
+
375
+ if (_.isUndefined(window['__mp_recorder'])) {
376
+ var scriptEl = document.createElement('script');
377
+ scriptEl.type = 'text/javascript';
378
+ scriptEl.async = true;
379
+ scriptEl.onload = handleLoadedRecorder;
380
+ scriptEl.src = this.get_config('recorder_src');
381
+ document.head.appendChild(scriptEl);
382
+ } else {
383
+ handleLoadedRecorder();
384
+ }
385
+ });
386
+
387
+ MixpanelLib.prototype.stop_session_recording = function () {
388
+ if (this._recorder) {
389
+ this._recorder['stopRecording']();
390
+ } else {
391
+ console.critical('Session recorder module not loaded');
348
392
  }
349
393
  };
350
394
 
@@ -353,12 +397,25 @@ MixpanelLib.prototype._init = function(token, config, name) {
353
397
  MixpanelLib.prototype._loaded = function() {
354
398
  this.get_config('loaded')(this);
355
399
  this._set_default_superprops();
400
+ this['people'].set_once(this['persistence'].get_referrer_info());
401
+
402
+ // The original 'store_google' functionality will be deprecated and the config will be
403
+ // used to clear previously managed UTM parameters from persistence.
404
+ // stop_utm_persistence is `false` by default now but will be default `true` in the future.
405
+ if (this.get_config('store_google') && this.get_config('stop_utm_persistence')) {
406
+ var utm_params = _.info.campaignParams(null);
407
+ _.each(utm_params, function(_utm_value, utm_key) {
408
+ // We need to unregister persisted UTM parameters so old values
409
+ // are not mixed with the new UTM parameters
410
+ this.unregister(utm_key);
411
+ }.bind(this));
412
+ }
356
413
  };
357
414
 
358
415
  // update persistence with info on referrer, UTM params, etc
359
416
  MixpanelLib.prototype._set_default_superprops = function() {
360
417
  this['persistence'].update_search_keyword(document.referrer);
361
- if (this.get_config('store_google')) {
418
+ if (this.get_config('store_google') && !this.get_config('stop_utm_persistence')) {
362
419
  this.register(_.info.campaignParams());
363
420
  }
364
421
  if (this.get_config('save_referrer')) {
@@ -396,6 +453,55 @@ MixpanelLib.prototype._track_dom = function(DomClass, args) {
396
453
  return dt.track.apply(dt, args);
397
454
  };
398
455
 
456
+ MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) {
457
+ var previous_tracked_url = '';
458
+ var tracked = this.track_pageview();
459
+ if (tracked) {
460
+ previous_tracked_url = _.info.currentUrl();
461
+ }
462
+
463
+ if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) {
464
+ window.addEventListener('popstate', function() {
465
+ window.dispatchEvent(new Event('mp_locationchange'));
466
+ });
467
+ window.addEventListener('hashchange', function() {
468
+ window.dispatchEvent(new Event('mp_locationchange'));
469
+ });
470
+ var nativePushState = window.history.pushState;
471
+ if (typeof nativePushState === 'function') {
472
+ window.history.pushState = function(state, unused, url) {
473
+ nativePushState.call(window.history, state, unused, url);
474
+ window.dispatchEvent(new Event('mp_locationchange'));
475
+ };
476
+ }
477
+ var nativeReplaceState = window.history.replaceState;
478
+ if (typeof nativeReplaceState === 'function') {
479
+ window.history.replaceState = function(state, unused, url) {
480
+ nativeReplaceState.call(window.history, state, unused, url);
481
+ window.dispatchEvent(new Event('mp_locationchange'));
482
+ };
483
+ }
484
+ window.addEventListener('mp_locationchange', function() {
485
+ var current_url = _.info.currentUrl();
486
+ var should_track = false;
487
+ if (track_pageview_option === 'full-url') {
488
+ should_track = current_url !== previous_tracked_url;
489
+ } else if (track_pageview_option === 'url-with-path-and-query-string') {
490
+ should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0];
491
+ } else if (track_pageview_option === 'url-with-path') {
492
+ should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0];
493
+ }
494
+
495
+ if (should_track) {
496
+ var tracked = this.track_pageview();
497
+ if (tracked) {
498
+ previous_tracked_url = current_url;
499
+ }
500
+ }
501
+ }.bind(this));
502
+ }
503
+ };
504
+
399
505
  /**
400
506
  * _prepare_callback() should be called by callers of _send_request for use
401
507
  * as the callback argument.
@@ -857,6 +963,13 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
857
963
  ? _.info.marketingParams()
858
964
  : {};
859
965
 
966
+ if (this._recorder) {
967
+ var replay_id = this._recorder['replayId'];
968
+ if (replay_id) {
969
+ properties['$mp_replay_id'] = replay_id;
970
+ }
971
+ }
972
+
860
973
  // note: extend writes to the first object, so lets make sure we
861
974
  // don't write to the persistence properties object and info
862
975
  // properties object by passing in a new object
@@ -864,7 +977,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
864
977
  // update properties with pageview info and super-properties
865
978
  properties = _.extend(
866
979
  {},
867
- _.info.properties(),
980
+ _.info.properties({'mp_loader': this.get_config('mp_loader')}),
868
981
  marketing_properties,
869
982
  this['persistence'].properties(),
870
983
  this.unpersisted_superprops,
@@ -1028,10 +1141,9 @@ MixpanelLib.prototype.get_group = function (group_key, group_id) {
1028
1141
 
1029
1142
  /**
1030
1143
  * Track a default Mixpanel page view event, which includes extra default event properties to
1031
- * improve page view data. The `config.track_pageview` option for <a href="#mixpanelinit">mixpanel.init()</a>
1032
- * may be turned on for tracking page loads automatically.
1144
+ * improve page view data.
1033
1145
  *
1034
- * ### Usage
1146
+ * ### Usage:
1035
1147
  *
1036
1148
  * // track a default $mp_web_page_view event
1037
1149
  * mixpanel.track_pageview();
@@ -1048,6 +1160,23 @@ MixpanelLib.prototype.get_group = function (group_key, group_id) {
1048
1160
  * // views on different products or internal applications that are considered completely separate
1049
1161
  * mixpanel.track_pageview({'page': 'customer-search'}, {'event_name': '[internal] Admin Page View'});
1050
1162
  *
1163
+ * ### Notes:
1164
+ *
1165
+ * The `config.track_pageview` option for <a href="#mixpanelinit">mixpanel.init()</a>
1166
+ * may be turned on for tracking page loads automatically.
1167
+ *
1168
+ * // track only page loads
1169
+ * mixpanel.init(PROJECT_TOKEN, {track_pageview: true});
1170
+ *
1171
+ * // track when the URL changes in any manner
1172
+ * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'full-url'});
1173
+ *
1174
+ * // track when the URL changes, ignoring any changes in the hash part
1175
+ * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path-and-query-string'});
1176
+ *
1177
+ * // track when the path changes, ignoring any query parameter or hash changes
1178
+ * mixpanel.init(PROJECT_TOKEN, {track_pageview: 'url-with-path'});
1179
+ *
1051
1180
  * @param {Object} [properties] An optional set of additional properties to send with the page view event
1052
1181
  * @param {Object} [options] Page view tracking options
1053
1182
  * @param {String} [options.event_name] - Alternate name for the tracking event
@@ -1798,7 +1927,7 @@ MixpanelLib.prototype._gdpr_call_func = function(func, options) {
1798
1927
  /**
1799
1928
  * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance
1800
1929
  *
1801
- * ### Usage
1930
+ * ### Usage:
1802
1931
  *
1803
1932
  * // opt user in
1804
1933
  * mixpanel.opt_in_tracking();
@@ -1838,7 +1967,7 @@ MixpanelLib.prototype.opt_in_tracking = function(options) {
1838
1967
  /**
1839
1968
  * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance
1840
1969
  *
1841
- * ### Usage
1970
+ * ### Usage:
1842
1971
  *
1843
1972
  * // opt user out
1844
1973
  * mixpanel.opt_out_tracking();
@@ -1879,7 +2008,7 @@ MixpanelLib.prototype.opt_out_tracking = function(options) {
1879
2008
  /**
1880
2009
  * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance
1881
2010
  *
1882
- * ### Usage
2011
+ * ### Usage:
1883
2012
  *
1884
2013
  * var has_opted_in = mixpanel.has_opted_in_tracking();
1885
2014
  * // use has_opted_in value
@@ -1896,7 +2025,7 @@ MixpanelLib.prototype.has_opted_in_tracking = function(options) {
1896
2025
  /**
1897
2026
  * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance
1898
2027
  *
1899
- * ### Usage
2028
+ * ### Usage:
1900
2029
  *
1901
2030
  * var has_opted_out = mixpanel.has_opted_out_tracking();
1902
2031
  * // use has_opted_out value
@@ -1913,7 +2042,7 @@ MixpanelLib.prototype.has_opted_out_tracking = function(options) {
1913
2042
  /**
1914
2043
  * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance
1915
2044
  *
1916
- * ### Usage
2045
+ * ### Usage:
1917
2046
  *
1918
2047
  * // clear user's opt-in/out status
1919
2048
  * mixpanel.clear_opt_in_out_tracking();
@@ -1990,6 +2119,8 @@ MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remov
1990
2119
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
1991
2120
  MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
1992
2121
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
2122
+ MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
2123
+ MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
1993
2124
  MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
1994
2125
 
1995
2126
  // MixpanelPersistence Exports
@@ -57,7 +57,6 @@ MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, c
57
57
  data[SET_ACTION] = _.extend(
58
58
  {},
59
59
  _.info.people_properties(),
60
- this._mixpanel['persistence'].get_referrer_info(),
61
60
  data[SET_ACTION]
62
61
  );
63
62
  return this._send_request(data, callback);
@@ -0,0 +1,153 @@
1
+ import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js';
2
+
3
+ import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase
4
+ import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
5
+
6
+ var logger = console_with_prefix('recorder');
7
+
8
+ var MixpanelRecorder = function(mixpanelInstance) {
9
+ this._mixpanel = mixpanelInstance;
10
+
11
+ // internal rrweb stopRecording function
12
+ this._stopRecording = null;
13
+
14
+ this.recEvents = [];
15
+ this.seqNo = 0;
16
+ this.replayId = null;
17
+ this.replayStartTime = null;
18
+ this.batchStartTime = null;
19
+ this.replayLengthMs = 0;
20
+ this.sendBatchId = null;
21
+
22
+ this.idleTimeoutId = null;
23
+ this.maxTimeoutId = null;
24
+
25
+ this.recordMaxMs = MAX_RECORDING_MS;
26
+ };
27
+
28
+ // eslint-disable-next-line camelcase
29
+ MixpanelRecorder.prototype.get_config = function(configVar) {
30
+ return this._mixpanel.get_config(configVar);
31
+ };
32
+
33
+ MixpanelRecorder.prototype.startRecording = function () {
34
+ if (this._stopRecording !== null) {
35
+ logger.log('Recording already in progress, skipping startRecording.');
36
+ return;
37
+ }
38
+
39
+ this.recordMaxMs = this.get_config('record_max_ms');
40
+ if (this.recordMaxMs > MAX_RECORDING_MS) {
41
+ this.recordMaxMs = MAX_RECORDING_MS;
42
+ logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
43
+ }
44
+
45
+ this.recEvents = [];
46
+ this.seqNo = 0;
47
+ this.startDate = new Date();
48
+ this.replayStartTime = this.startDate.getTime();
49
+ this.batchStartTime = this.replayStartTime;
50
+
51
+ this.replayId = _.UUID();
52
+ this.replayLengthMs = 0;
53
+
54
+ var resetIdleTimeout = _.bind(function () {
55
+ clearTimeout(this.idleTimeoutId);
56
+ this.idleTimeoutId = setTimeout(_.bind(function () {
57
+ logger.log('Idle timeout reached, restarting recording.');
58
+ this.resetRecording();
59
+ }, this), this.get_config('record_idle_timeout_ms'));
60
+ }, this);
61
+
62
+ this._stopRecording = record({
63
+ 'emit': _.bind(function (ev) {
64
+ this.recEvents.push(ev);
65
+ this.replayLengthMs = new Date().getTime() - this.replayStartTime;
66
+ resetIdleTimeout();
67
+ }, this),
68
+ 'maskAllInputs': true,
69
+ 'maskTextSelector': this.get_config('record_mask_text_selector')
70
+ });
71
+
72
+ resetIdleTimeout();
73
+
74
+ this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000);
75
+ this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
76
+ };
77
+
78
+ MixpanelRecorder.prototype.resetRecording = function () {
79
+ this.stopRecording();
80
+ this.startRecording();
81
+ };
82
+
83
+ MixpanelRecorder.prototype.stopRecording = function () {
84
+ if (this._stopRecording !== null) {
85
+ this._stopRecording();
86
+ this._stopRecording = null;
87
+ }
88
+
89
+ this._flushEvents(); // flush any remaining events
90
+ this.replayId = null;
91
+
92
+ clearInterval(this.sendBatchId);
93
+ clearTimeout(this.idleTimeoutId);
94
+ clearTimeout(this.maxTimeoutId);
95
+ };
96
+
97
+ /**
98
+ * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
99
+ * we stop recording and dump any queued events if the user has opted out.
100
+ */
101
+ MixpanelRecorder.prototype.flushEventsWithOptOut = function () {
102
+ this._flushEvents(_.bind(this._onOptOut, this));
103
+ };
104
+
105
+ MixpanelRecorder.prototype._onOptOut = function (code) {
106
+ // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
107
+ if (code === 0) {
108
+ this.recEvents = [];
109
+ this.stopRecording();
110
+ }
111
+ };
112
+
113
+ /**
114
+ * @api private
115
+ * Private method, flushes the current batch of events to the server.
116
+ */
117
+ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
118
+ var numEvents = this.recEvents.length;
119
+ if (numEvents > 0) {
120
+ var reqBody = {
121
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
122
+ 'events': this.recEvents,
123
+ 'seq': this.seqNo++,
124
+ 'batch_start_time': this.batchStartTime / 1000,
125
+ 'replay_id': this.replayId,
126
+ 'replay_length_ms': this.replayLengthMs,
127
+ 'replay_start_time': this.replayStartTime / 1000
128
+ };
129
+
130
+ // send ID management props if they exist
131
+ var deviceId = this._mixpanel.get_property('$device_id');
132
+ if (deviceId) {
133
+ reqBody['$device_id'] = deviceId;
134
+ }
135
+ var userId = this._mixpanel.get_property('$user_id');
136
+ if (userId) {
137
+ reqBody['$user_id'] = userId;
138
+ }
139
+
140
+ window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'], {
141
+ 'method': 'POST',
142
+ 'headers': {
143
+ 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
144
+ 'Content-Type': 'application/json'
145
+ },
146
+ 'body': _.JSONEncode(reqBody)
147
+ });
148
+ this.recEvents = this.recEvents.slice(numEvents);
149
+ this.batchStartTime = new Date().getTime();
150
+ }
151
+ });
152
+
153
+ window['__mp_recorder'] = MixpanelRecorder;
@@ -0,0 +1,19 @@
1
+ import esbuild from 'rollup-plugin-esbuild';
2
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
3
+
4
+ export default {
5
+ input: 'index.js',
6
+ output: [
7
+ {
8
+ file: 'build/mixpanel-recorder.js',
9
+ format: 'esm'
10
+ },
11
+ {
12
+ file: 'build/mixpanel-recorder.min.js',
13
+ format: 'esm',
14
+ name: 'version',
15
+ plugins: [esbuild({minify: true})]
16
+ }
17
+ ],
18
+ plugins: [nodeResolve({browser: true})],
19
+ };
package/src/utils.js CHANGED
@@ -20,6 +20,9 @@ if (typeof(window) === 'undefined') {
20
20
  win = window;
21
21
  }
22
22
 
23
+ // Maximum allowed session recording length
24
+ var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
25
+
23
26
  /*
24
27
  * Saved references to long variable names, so that closure compiler can
25
28
  * minimize file size.
@@ -899,6 +902,7 @@ _.UUID = (function() {
899
902
  // sending false tracking data
900
903
  var BLOCKED_UA_STRS = [
901
904
  'ahrefsbot',
905
+ 'ahrefssiteaudit',
902
906
  'baiduspider',
903
907
  'bingbot',
904
908
  'bingpreview',
@@ -1619,7 +1623,14 @@ _.info = {
1619
1623
  return '';
1620
1624
  },
1621
1625
 
1622
- properties: function() {
1626
+ currentUrl: function() {
1627
+ return win.location.href;
1628
+ },
1629
+
1630
+ properties: function(extra_props) {
1631
+ if (typeof extra_props !== 'object') {
1632
+ extra_props = {};
1633
+ }
1623
1634
  return _.extend(_.strip_empty_properties({
1624
1635
  '$os': _.info.os(),
1625
1636
  '$browser': _.info.browser(userAgent, navigator.vendor, windowOpera),
@@ -1627,7 +1638,7 @@ _.info = {
1627
1638
  '$referring_domain': _.info.referringDomain(document.referrer),
1628
1639
  '$device': _.info.device(userAgent)
1629
1640
  }), {
1630
- '$current_url': win.location.href,
1641
+ '$current_url': _.info.currentUrl(),
1631
1642
  '$browser_version': _.info.browserVersion(userAgent, navigator.vendor, windowOpera),
1632
1643
  '$screen_height': screen.height,
1633
1644
  '$screen_width': screen.width,
@@ -1635,7 +1646,7 @@ _.info = {
1635
1646
  '$lib_version': Config.LIB_VERSION,
1636
1647
  '$insert_id': cheap_guid(),
1637
1648
  'time': _.timestamp() / 1000 // epoch time in seconds
1638
- });
1649
+ }, _.strip_empty_properties(extra_props));
1639
1650
  },
1640
1651
 
1641
1652
  people_properties: function() {
@@ -1713,6 +1724,7 @@ _['info']['browserVersion'] = _.info.browserVersion;
1713
1724
  _['info']['properties'] = _.info.properties;
1714
1725
 
1715
1726
  export {
1727
+ MAX_RECORDING_MS,
1716
1728
  _,
1717
1729
  userAgent,
1718
1730
  console,
@@ -1,4 +0,0 @@
1
- /* eslint camelcase: "off" */
2
- import { init_from_snippet } from './mixpanel-core';
3
-
4
- init_from_snippet();