mixpanel-browser 2.54.1 → 2.55.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ **2.55.1** (27 Aug 2024)
2
+ - Adds a minimum recording length option for session recording
3
+ - Fixes and improvements for session recording batcher to support offline queueing and retry
4
+ - Fix for query param parsing/escaping
5
+ - Support for more UTM tags / click IDs (thanks @aliyalcinkaya)
6
+
7
+ **2.55.0** (2 Aug 2024)
8
+ - Added new build to support native JavaScript modules
9
+
1
10
  **2.54.1** (30 Jul 2024)
2
11
  - Fixes and improvements for user-idleness detection in session recording
3
12
 
package/README.md CHANGED
@@ -8,16 +8,16 @@ intended to be used by websites wishing to send data to Mixpanel projects. A ful
8
8
  is available [here](https://developer.mixpanel.com/docs/javascript-full-api-reference).
9
9
 
10
10
  ## Alternative installation via NPM
11
- This library is available as a [package on NPM](https://www.npmjs.com/package/mixpanel-browser) (named `mixpanel-browser` to distinguish it from Mixpanel's server-side Node.js library, available on NPM as `mixpanel`). To install into a project using NPM with a front-end packager such as [Browserify](http://browserify.org/) or [Webpack](https://webpack.github.io/):
11
+ This library is available as a [package on NPM](https://www.npmjs.com/package/mixpanel-browser) (named `mixpanel-browser` to distinguish it from Mixpanel's server-side Node.js library, available on NPM as `mixpanel`). To install into a project using NPM with a front-end packager such as [Vite](https://vitejs.dev/) or [Webpack](https://webpack.github.io/):
12
12
 
13
13
  ```sh
14
14
  npm install --save mixpanel-browser
15
15
  ```
16
16
 
17
- You can then require the lib like a standard Node.js module:
17
+ You can then import the lib:
18
18
 
19
19
  ```javascript
20
- var mixpanel = require('mixpanel-browser');
20
+ import mixpanel from 'mixpanel-browser';
21
21
 
22
22
  mixpanel.init("YOUR_TOKEN");
23
23
  mixpanel.track("An event");
@@ -35,38 +35,29 @@ To load the core SDK and optionally load session recording bundle asynchronously
35
35
  import mixpanel from 'mixpanel-browser/src/loaders/loader-module-with-async-recorder';
36
36
  ```
37
37
 
38
- ## Alternative installation via Bower
39
- `mixpanel-js` is also available via front-end package manager [Bower](http://bower.io/). After installing Bower, fetch into your project's `bower_components` dir with:
40
- ```sh
41
- bower install mixpanel
42
- ```
43
-
44
- ### Using Bower to load the snippet
45
- You can then load the lib via the embed code (snippet) with a script reference:
46
- ```html
47
- <script src="bower_components/mixpanel/mixpanel-jslib-snippet.min.js"></script>
48
- ```
49
- which loads the _latest_ library version from the Mixpanel CDN ([http://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js](http://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js)).
38
+ ## Use as a browser JavaScript module
50
39
 
51
- ### Using Bower to load the entire library
52
- If you wish to load the specific version downloaded in your Bower package, there are two options.
40
+ If you are leveraging [browser JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), you can use [`importmap`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to pull in this library.
53
41
 
54
- 1) Override the CDN library location with the global `MIXPANEL_CUSTOM_LIB_URL` var:
55
42
  ```html
56
- <script>
57
- window.MIXPANEL_CUSTOM_LIB_URL = 'bower_components/mixpanel/mixpanel.js';
43
+ <script type="importmap">
44
+ {
45
+ "imports": {
46
+ "mixpanel-browser": "https://cdn.mxpnl.com/libs/mixpanel-js/dist/mixpanel.module.js"
47
+ }
48
+ }
58
49
  </script>
59
- <script src="bower_components/mixpanel/mixpanel-jslib-snippet.min.js"></script>
50
+ <script type="module" src="main.js"></script>
60
51
  ```
61
- or
62
52
 
63
- 2) Recompile the snippet with a custom `MIXPANEL_LIB_URL` using [Closure Compiler](https://developers.google.com/closure/compiler/):
64
- ```sh
65
- java -jar compiler.jar --js src/loaders/mixpanel-jslib-snippet.js --js_output_file mixpanel-jslib-snippet.min.js --compilation_level ADVANCED_OPTIMIZATIONS --define='MIXPANEL_LIB_URL="bower_components/mixpanel/mixpanel.js"'
66
- ```
53
+ Then you are free to import `mixpanel-browser` in your javascript modules.
67
54
 
68
- ### Upgrading from mixpanel-bower v2.2.0 or v2.0.0
69
- If you originally installed Mixpanel via Bower at its previous home ([https://github.com/drubin/mixpanel-bower](https://github.com/drubin/mixpanel-bower)), the two old versions have remained functionally unchanged. To upgrade to v2.3.6 or later (the first Bower version in the official repo) from a previous Bower install, note the changed filenames: previous references to `mixpanel.js` should become `mixpanel-jslib-snippet.min.js` (the minified embed code), and previous references to `mixpanel.dev.js` should become `mixpanel.js` (the library source) or `mixpanel.min.js` (the minified library for production use).
55
+ ```js
56
+ // main.js
57
+ import mixpanel from 'mixpanel-browser';
58
+
59
+ mixpanel.init('YOUR_TOKEN', {debug: true, track_pageview: true, persistence: 'localStorage'});
60
+ ```
70
61
 
71
62
  ## Building bundles for release
72
63
  - Install development dependencies: `npm install`
@@ -87,4 +78,4 @@ Mixpanel production releases are tested against a large matrix of browsers and o
87
78
  - Publish to readme.io via the [rdme](https://www.npmjs.com/package/rdme) util: `RDME_API_KEY=<API_KEY> RDME_DOC_VERSION=<version> npm run dox-publish`
88
79
 
89
80
  ## Thanks
90
- For patches and support: @bohanyang, @dehau, @drubin, @D1plo1d, @feychenie, @mogstad, @pfhayes, @sandorfr, @stefansedich, @gfx, @pkaminski, @austince, @danielbaker, @mkdai, @wolever, @dpraul, @chriszamierowski, @JoaoGomesTW
81
+ For patches and support: @bohanyang, @dehau, @drubin, @D1plo1d, @feychenie, @mogstad, @pfhayes, @sandorfr, @stefansedich, @gfx, @pkaminski, @austince, @danielbaker, @mkdai, @wolever, @dpraul, @chriszamierowski, @JoaoGomesTW, @@aliyalcinkaya
package/build.sh CHANGED
@@ -27,6 +27,7 @@ if [ ! -z "$FULL" ]; then
27
27
  echo 'Building module bundles'
28
28
  npx rollup -i src/loaders/loader-module.js -f amd -o build/mixpanel.amd.js -c rollup.config.js
29
29
  npx rollup -i src/loaders/loader-module.js -f cjs -o build/mixpanel.cjs.js -c rollup.config.js
30
+ npx rollup -i src/loaders/loader-module.js -f es -o build/mixpanel.module.js -c rollup.config.js
30
31
  npx rollup -i src/loaders/loader-module-core.js -f cjs -o build/mixpanel-core.cjs.js -c rollup.config.js
31
32
  npx rollup -i src/loaders/loader-module-with-async-recorder.js -f cjs -o build/mixpanel-with-async-recorder.cjs.js -c rollup.config.js
32
33
  npx rollup -i src/loaders/loader-module.js -f umd -o build/mixpanel.umd.js -n mixpanel -c rollup.config.js
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.54.1'
5
+ LIB_VERSION: '2.55.1'
6
6
  };
7
7
 
8
8
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -14,7 +14,7 @@ if (typeof(window) === 'undefined') {
14
14
  hostname: ''
15
15
  };
16
16
  win = {
17
- navigator: { userAgent: '' },
17
+ navigator: { userAgent: '', onLine: true },
18
18
  document: {
19
19
  location: loc,
20
20
  referrer: ''
@@ -968,7 +968,7 @@ _.HTTPBuildQuery = function(formdata, arg_separator) {
968
968
  _.getQueryParam = function(url, param) {
969
969
  // Expects a raw URL
970
970
 
971
- param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
971
+ param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
972
972
  var regexS = '[\\?&]' + param + '=([^&#]*)',
973
973
  regex = new RegExp(regexS),
974
974
  results = regex.exec(url);
@@ -1425,8 +1425,8 @@ _.dom_query = (function() {
1425
1425
  };
1426
1426
  })();
1427
1427
 
1428
- var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
1429
- var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
1428
+ var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic'];
1429
+ var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
1430
1430
 
1431
1431
  _.info = {
1432
1432
  campaignParams: function(default_value) {
@@ -1708,6 +1708,15 @@ var extract_domain = function(hostname) {
1708
1708
  return matches ? matches[0] : '';
1709
1709
  };
1710
1710
 
1711
+ /**
1712
+ * Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
1713
+ * @returns {boolean}
1714
+ */
1715
+ var isOnline = function() {
1716
+ var onLine = win.navigator['onLine'];
1717
+ return _.isUndefined(onLine) || onLine;
1718
+ };
1719
+
1711
1720
  var JSONStringify = null, JSONParse = null;
1712
1721
  if (typeof JSON !== 'undefined') {
1713
1722
  JSONStringify = JSON.stringify;
@@ -2523,7 +2532,12 @@ RequestBatcher.prototype.flush = function(options) {
2523
2532
  this.flush();
2524
2533
  } else if (
2525
2534
  _.isObject(res) &&
2526
- (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
2535
+ (
2536
+ res.httpStatusCode >= 500
2537
+ || res.httpStatusCode === 429
2538
+ || (res.httpStatusCode <= 0 && !isOnline())
2539
+ || res.error === 'timeout'
2540
+ )
2527
2541
  ) {
2528
2542
  // network or API error, or 429 Too Many Requests, retry
2529
2543
  var retryMS = this.flushInterval * 2;
@@ -4247,6 +4261,7 @@ var DEFAULT_CONFIG = {
4247
4261
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
4248
4262
  'record_mask_text_selector': '*',
4249
4263
  'record_max_ms': MAX_RECORDING_MS,
4264
+ 'record_min_ms': 0,
4250
4265
  'record_sessions_percent': 0,
4251
4266
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
4252
4267
  };
@@ -4510,7 +4510,7 @@
4510
4510
 
4511
4511
  var Config = {
4512
4512
  DEBUG: false,
4513
- LIB_VERSION: '2.54.1'
4513
+ LIB_VERSION: '2.55.1'
4514
4514
  };
4515
4515
 
4516
4516
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -4522,7 +4522,7 @@
4522
4522
  hostname: ''
4523
4523
  };
4524
4524
  win = {
4525
- navigator: { userAgent: '' },
4525
+ navigator: { userAgent: '', onLine: true },
4526
4526
  document: {
4527
4527
  location: loc,
4528
4528
  referrer: ''
@@ -4536,6 +4536,8 @@
4536
4536
 
4537
4537
  // Maximum allowed session recording length
4538
4538
  var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
4539
+ // Maximum allowed value for minimum session recording length
4540
+ var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
4539
4541
 
4540
4542
  /*
4541
4543
  * Saved references to long variable names, so that closure compiler can
@@ -5447,7 +5449,7 @@
5447
5449
  _.getQueryParam = function(url, param) {
5448
5450
  // Expects a raw URL
5449
5451
 
5450
- param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
5452
+ param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
5451
5453
  var regexS = '[\\?&]' + param + '=([^&#]*)',
5452
5454
  regex = new RegExp(regexS),
5453
5455
  results = regex.exec(url);
@@ -5904,8 +5906,8 @@
5904
5906
  };
5905
5907
  })();
5906
5908
 
5907
- var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
5908
- var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
5909
+ var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic'];
5910
+ var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
5909
5911
 
5910
5912
  _.info = {
5911
5913
  campaignParams: function(default_value) {
@@ -6187,6 +6189,15 @@
6187
6189
  return matches ? matches[0] : '';
6188
6190
  };
6189
6191
 
6192
+ /**
6193
+ * Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
6194
+ * @returns {boolean}
6195
+ */
6196
+ var isOnline = function() {
6197
+ var onLine = win.navigator['onLine'];
6198
+ return _.isUndefined(onLine) || onLine;
6199
+ };
6200
+
6190
6201
  var JSONStringify = null, JSONParse = null;
6191
6202
  if (typeof JSON !== 'undefined') {
6192
6203
  JSONStringify = JSON.stringify;
@@ -7018,7 +7029,12 @@
7018
7029
  this.flush();
7019
7030
  } else if (
7020
7031
  _.isObject(res) &&
7021
- (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
7032
+ (
7033
+ res.httpStatusCode >= 500
7034
+ || res.httpStatusCode === 429
7035
+ || (res.httpStatusCode <= 0 && !isOnline())
7036
+ || res.error === 'timeout'
7037
+ )
7022
7038
  ) {
7023
7039
  // network or API error, or 429 Too Many Requests, retry
7024
7040
  var retryMS = this.flushInterval * 2;
@@ -7169,6 +7185,7 @@
7169
7185
  this.maxTimeoutId = null;
7170
7186
 
7171
7187
  this.recordMaxMs = MAX_RECORDING_MS;
7188
+ this.recordMinMs = 0;
7172
7189
  this._initBatcher();
7173
7190
  };
7174
7191
 
@@ -7200,16 +7217,24 @@
7200
7217
  logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7201
7218
  }
7202
7219
 
7220
+ this.recordMinMs = this.get_config('record_min_ms');
7221
+ if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7222
+ this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7223
+ logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7224
+ }
7225
+
7203
7226
  this.recEvents = [];
7204
7227
  this.seqNo = 0;
7205
- this.replayStartTime = null;
7228
+ this.replayStartTime = new Date().getTime();
7206
7229
 
7207
7230
  this.replayId = _.UUID();
7208
7231
 
7209
- if (shouldStopBatcher) {
7210
- // this is the case when we're starting recording after a reset
7232
+ if (shouldStopBatcher || this.recordMinMs > 0) {
7233
+ // the primary case for shouldStopBatcher is when we're starting recording after a reset
7211
7234
  // and don't want to send anything over the network until there's
7212
7235
  // actual user activity
7236
+ // this also applies if the minimum recording length has not been hit yet
7237
+ // so that we don't send data until we know the recording will be long enough
7213
7238
  this.batcher.stop();
7214
7239
  } else {
7215
7240
  this.batcher.start();
@@ -7223,11 +7248,16 @@
7223
7248
  }, this), this.get_config('record_idle_timeout_ms'));
7224
7249
  }, this);
7225
7250
 
7251
+ var blockSelector = this.get_config('record_block_selector');
7252
+ if (blockSelector === '' || blockSelector === null) {
7253
+ blockSelector = undefined;
7254
+ }
7255
+
7226
7256
  this._stopRecording = record({
7227
7257
  'emit': _.bind(function (ev) {
7228
7258
  this.batcher.enqueue(ev);
7229
7259
  if (isUserEvent(ev)) {
7230
- if (this.batcher.stopped) {
7260
+ if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
7231
7261
  // start flushing again after user activity
7232
7262
  this.batcher.start();
7233
7263
  }
@@ -7235,7 +7265,7 @@
7235
7265
  }
7236
7266
  }, this),
7237
7267
  'blockClass': this.get_config('record_block_class'),
7238
- 'blockSelector': this.get_config('record_block_selector'),
7268
+ 'blockSelector': blockSelector,
7239
7269
  'collectFonts': this.get_config('record_collect_fonts'),
7240
7270
  'inlineImages': this.get_config('record_inline_images'),
7241
7271
  'maskAllInputs': true,
@@ -7289,14 +7319,14 @@
7289
7319
  }
7290
7320
  };
7291
7321
 
7292
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
7322
+ MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7293
7323
  var onSuccess = _.bind(function (response, responseBody) {
7294
7324
  // Increment sequence counter only if the request was successful to guarantee ordering.
7295
7325
  // RequestBatcher will always flush the next batch after the previous one succeeds.
7296
- if (response.status === 200) {
7326
+ // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
7327
+ if (response.status === 200 && this.replayId === currentReplayId) {
7297
7328
  this.seqNo++;
7298
7329
  }
7299
-
7300
7330
  callback({
7301
7331
  status: 0,
7302
7332
  httpStatusCode: response.status,
@@ -7319,7 +7349,7 @@
7319
7349
  callback({error: error});
7320
7350
  });
7321
7351
  }).catch(function (error) {
7322
- callback({error: error});
7352
+ callback({error: error, httpStatusCode: 0});
7323
7353
  });
7324
7354
  };
7325
7355
 
@@ -7327,9 +7357,15 @@
7327
7357
  const numEvents = data.length;
7328
7358
 
7329
7359
  if (numEvents > 0) {
7360
+ var replayId = this.replayId;
7330
7361
  // each rrweb event has a timestamp - leverage those to get time properties
7331
7362
  var batchStartTime = data[0].timestamp;
7332
- if (this.seqNo === 0) {
7363
+ if (this.seqNo === 0 || !this.replayStartTime) {
7364
+ // extra safety net so that we don't send a null replay start time
7365
+ if (this.seqNo !== 0) {
7366
+ this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
7367
+ }
7368
+
7333
7369
  this.replayStartTime = batchStartTime;
7334
7370
  }
7335
7371
  var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
@@ -7338,7 +7374,7 @@
7338
7374
  'distinct_id': String(this._mixpanel.get_distinct_id()),
7339
7375
  'seq': this.seqNo,
7340
7376
  'batch_start_time': batchStartTime / 1000,
7341
- 'replay_id': this.replayId,
7377
+ 'replay_id': replayId,
7342
7378
  'replay_length_ms': replayLengthMs,
7343
7379
  'replay_start_time': this.replayStartTime / 1000
7344
7380
  };
@@ -7361,11 +7397,11 @@
7361
7397
  .blob()
7362
7398
  .then(_.bind(function(compressedBlob) {
7363
7399
  reqParams['format'] = 'gzip';
7364
- this._sendRequest(reqParams, compressedBlob, callback);
7400
+ this._sendRequest(replayId, reqParams, compressedBlob, callback);
7365
7401
  }, this));
7366
7402
  } else {
7367
7403
  reqParams['format'] = 'body';
7368
- this._sendRequest(reqParams, eventsJson, callback);
7404
+ this._sendRequest(replayId, reqParams, eventsJson, callback);
7369
7405
  }
7370
7406
  }
7371
7407
  });