mixpanel-browser 2.59.0 → 2.61.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.59.0'
5
+ LIB_VERSION: '2.61.0'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -1466,15 +1466,9 @@ _.cookie = {
1466
1466
  }
1467
1467
  };
1468
1468
 
1469
- var _localStorageSupported = null;
1470
- var localStorageSupported = function(storage, forceCheck) {
1471
- if (_localStorageSupported !== null && !forceCheck) {
1472
- return _localStorageSupported;
1473
- }
1474
-
1469
+ var _testStorageSupported = function (storage) {
1475
1470
  var supported = true;
1476
1471
  try {
1477
- storage = storage || win.localStorage;
1478
1472
  var key = '__mplss_' + cheap_guid(8),
1479
1473
  val = 'xyz';
1480
1474
  storage.setItem(key, val);
@@ -1485,59 +1479,74 @@ var localStorageSupported = function(storage, forceCheck) {
1485
1479
  } catch (err) {
1486
1480
  supported = false;
1487
1481
  }
1488
-
1489
- _localStorageSupported = supported;
1490
1482
  return supported;
1491
1483
  };
1492
1484
 
1493
- // _.localStorage
1494
- _.localStorage = {
1495
- is_supported: function(force_check) {
1496
- var supported = localStorageSupported(null, force_check);
1497
- if (!supported) {
1498
- console.error('localStorage unsupported; falling back to cookie store');
1499
- }
1500
- return supported;
1501
- },
1502
-
1503
- error: function(msg) {
1504
- console.error('localStorage error: ' + msg);
1505
- },
1485
+ var _localStorageSupported = null;
1486
+ var localStorageSupported = function(storage, forceCheck) {
1487
+ if (_localStorageSupported !== null && !forceCheck) {
1488
+ return _localStorageSupported;
1489
+ }
1490
+ return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
1491
+ };
1506
1492
 
1507
- get: function(name) {
1508
- try {
1509
- return win.localStorage.getItem(name);
1510
- } catch (err) {
1511
- _.localStorage.error(err);
1512
- }
1513
- return null;
1514
- },
1493
+ var _sessionStorageSupported = null;
1494
+ var sessionStorageSupported = function(storage, forceCheck) {
1495
+ if (_sessionStorageSupported !== null && !forceCheck) {
1496
+ return _sessionStorageSupported;
1497
+ }
1498
+ return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
1499
+ };
1515
1500
 
1516
- parse: function(name) {
1517
- try {
1518
- return _.JSONDecode(_.localStorage.get(name)) || {};
1519
- } catch (err) {
1520
- // noop
1521
- }
1522
- return null;
1523
- },
1501
+ function _storageWrapper(storage, name, is_supported_fn) {
1502
+ var log_error = function(msg) {
1503
+ console.error(name + ' error: ' + msg);
1504
+ };
1524
1505
 
1525
- set: function(name, value) {
1526
- try {
1527
- win.localStorage.setItem(name, value);
1528
- } catch (err) {
1529
- _.localStorage.error(err);
1506
+ return {
1507
+ is_supported: function(forceCheck) {
1508
+ var supported = is_supported_fn(storage, forceCheck);
1509
+ if (!supported) {
1510
+ console.error(name + ' unsupported');
1511
+ }
1512
+ return supported;
1513
+ },
1514
+ error: log_error,
1515
+ get: function(key) {
1516
+ try {
1517
+ return storage.getItem(key);
1518
+ } catch (err) {
1519
+ log_error(err);
1520
+ }
1521
+ return null;
1522
+ },
1523
+ parse: function(key) {
1524
+ try {
1525
+ return _.JSONDecode(storage.getItem(key)) || {};
1526
+ } catch (err) {
1527
+ // noop
1528
+ }
1529
+ return null;
1530
+ },
1531
+ set: function(key, value) {
1532
+ try {
1533
+ storage.setItem(key, value);
1534
+ } catch (err) {
1535
+ log_error(err);
1536
+ }
1537
+ },
1538
+ remove: function(key) {
1539
+ try {
1540
+ storage.removeItem(key);
1541
+ } catch (err) {
1542
+ log_error(err);
1543
+ }
1530
1544
  }
1531
- },
1545
+ };
1546
+ }
1532
1547
 
1533
- remove: function(name) {
1534
- try {
1535
- win.localStorage.removeItem(name);
1536
- } catch (err) {
1537
- _.localStorage.error(err);
1538
- }
1539
- }
1540
- };
1548
+ _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
1549
+ _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
1541
1550
 
1542
1551
  _.register_event = (function() {
1543
1552
  // written by Dean Edwards, 2005
@@ -2064,6 +2073,31 @@ _.info = {
2064
2073
  }
2065
2074
  };
2066
2075
 
2076
+ /**
2077
+ * Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
2078
+ * Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
2079
+ */
2080
+ var batchedThrottle = function (fn, waitMs) {
2081
+ var timeoutPromise = null;
2082
+ var throttledItems = [];
2083
+ return function (item) {
2084
+ var self = this;
2085
+ throttledItems.push(item);
2086
+
2087
+ if (!timeoutPromise) {
2088
+ timeoutPromise = new PromisePolyfill(function (resolve) {
2089
+ setTimeout(function () {
2090
+ var returnValue = fn.apply(self, [throttledItems]);
2091
+ timeoutPromise = null;
2092
+ throttledItems = [];
2093
+ resolve(returnValue);
2094
+ }, waitMs);
2095
+ });
2096
+ }
2097
+ return timeoutPromise;
2098
+ };
2099
+ };
2100
+
2067
2101
  var cheap_guid = function(maxlen) {
2068
2102
  var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
2069
2103
  return maxlen ? guid.substring(0, maxlen) : guid;
@@ -2106,6 +2140,8 @@ var isOnline = function() {
2106
2140
  return _.isUndefined(onLine) || onLine;
2107
2141
  };
2108
2142
 
2143
+ var NOOP_FUNC = function () {};
2144
+
2109
2145
  var JSONStringify = null, JSONParse = null;
2110
2146
  if (typeof JSON !== 'undefined') {
2111
2147
  JSONStringify = JSON.stringify;
@@ -2114,20 +2150,29 @@ if (typeof JSON !== 'undefined') {
2114
2150
  JSONStringify = JSONStringify || _.JSONEncode;
2115
2151
  JSONParse = JSONParse || _.JSONDecode;
2116
2152
 
2117
- // EXPORTS (for closure compiler)
2118
- _['toArray'] = _.toArray;
2119
- _['isObject'] = _.isObject;
2120
- _['JSONEncode'] = _.JSONEncode;
2121
- _['JSONDecode'] = _.JSONDecode;
2122
- _['isBlockedUA'] = _.isBlockedUA;
2123
- _['isEmptyObject'] = _.isEmptyObject;
2153
+ // UNMINIFIED EXPORTS (for closure compiler)
2124
2154
  _['info'] = _.info;
2125
- _['info']['device'] = _.info.device;
2126
2155
  _['info']['browser'] = _.info.browser;
2127
2156
  _['info']['browserVersion'] = _.info.browserVersion;
2157
+ _['info']['device'] = _.info.device;
2128
2158
  _['info']['properties'] = _.info.properties;
2159
+ _['isBlockedUA'] = _.isBlockedUA;
2160
+ _['isEmptyObject'] = _.isEmptyObject;
2161
+ _['isObject'] = _.isObject;
2162
+ _['JSONDecode'] = _.JSONDecode;
2163
+ _['JSONEncode'] = _.JSONEncode;
2164
+ _['toArray'] = _.toArray;
2129
2165
  _['NPO'] = NpoPromise;
2130
2166
 
2167
+ /**
2168
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
2169
+ * @returns {boolean}
2170
+ */
2171
+ var isRecordingExpired = function(serializedRecording) {
2172
+ var now = Date.now();
2173
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
2174
+ };
2175
+
2131
2176
  // stateless utils
2132
2177
 
2133
2178
  var EV_CHANGE = 'change';
@@ -2196,7 +2241,7 @@ function getPreviousElementSibling(el) {
2196
2241
  }
2197
2242
  }
2198
2243
 
2199
- function getPropertiesFromElement(el) {
2244
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
2200
2245
  var props = {
2201
2246
  '$classes': getClassName(el).split(' '),
2202
2247
  '$tag_name': el.tagName.toLowerCase()
@@ -2206,9 +2251,9 @@ function getPropertiesFromElement(el) {
2206
2251
  props['$id'] = elId;
2207
2252
  }
2208
2253
 
2209
- if (shouldTrackElement(el)) {
2210
- _.each(TRACKED_ATTRS, function(attr) {
2211
- if (el.hasAttribute(attr)) {
2254
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
2255
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
2256
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
2212
2257
  var attrVal = el.getAttribute(attr);
2213
2258
  if (shouldTrackValue(attrVal)) {
2214
2259
  props['$attr-' + attr] = attrVal;
@@ -2232,8 +2277,21 @@ function getPropertiesFromElement(el) {
2232
2277
  return props;
2233
2278
  }
2234
2279
 
2235
- function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2236
- blockSelectors = blockSelectors || [];
2280
+ function getPropsForDOMEvent(ev, config) {
2281
+ var allowElementCallback = config.allowElementCallback;
2282
+ var allowSelectors = config.allowSelectors || [];
2283
+ var blockAttrs = config.blockAttrs || [];
2284
+ var blockElementCallback = config.blockElementCallback;
2285
+ var blockSelectors = config.blockSelectors || [];
2286
+ var captureTextContent = config.captureTextContent || false;
2287
+ var captureExtraAttrs = config.captureExtraAttrs || [];
2288
+
2289
+ // convert array to set every time, as the config may have changed
2290
+ var blockAttrsSet = {};
2291
+ _.each(blockAttrs, function(attr) {
2292
+ blockAttrsSet[attr] = true;
2293
+ });
2294
+
2237
2295
  var props = null;
2238
2296
 
2239
2297
  var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
@@ -2241,7 +2299,11 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2241
2299
  target = target.parentNode;
2242
2300
  }
2243
2301
 
2244
- if (shouldTrackDomEvent(target, ev)) {
2302
+ if (
2303
+ shouldTrackDomEvent(target, ev) &&
2304
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
2305
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2306
+ ) {
2245
2307
  var targetElementList = [target];
2246
2308
  var curEl = target;
2247
2309
  while (curEl.parentNode && !isTag(curEl, 'body')) {
@@ -2252,37 +2314,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2252
2314
  var elementsJson = [];
2253
2315
  var href, explicitNoTrack = false;
2254
2316
  _.each(targetElementList, function(el) {
2255
- var shouldTrackEl = shouldTrackElement(el);
2317
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
2256
2318
 
2257
2319
  // if the element or a parent element is an anchor tag
2258
2320
  // include the href as a property
2259
- if (el.tagName.toLowerCase() === 'a') {
2321
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
2260
2322
  href = el.getAttribute('href');
2261
- href = shouldTrackEl && shouldTrackValue(href) && href;
2323
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
2262
2324
  }
2263
2325
 
2264
- // allow users to programmatically prevent tracking of elements by adding classes such as 'mp-no-track'
2265
- var classes = getClasses(el);
2266
- _.each(OPT_OUT_CLASSES, function(cls) {
2267
- if (classes[cls]) {
2268
- explicitNoTrack = true;
2269
- }
2270
- });
2271
-
2272
- if (!explicitNoTrack) {
2273
- // programmatically prevent tracking of elements that match CSS selectors
2274
- _.each(blockSelectors, function(sel) {
2275
- try {
2276
- if (el['matches'](sel)) {
2277
- explicitNoTrack = true;
2278
- }
2279
- } catch (err) {
2280
- logger$3.critical('Error while checking selector: ' + sel, err);
2281
- }
2282
- });
2326
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
2327
+ explicitNoTrack = true;
2283
2328
  }
2284
2329
 
2285
- elementsJson.push(getPropertiesFromElement(el));
2330
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
2286
2331
  }, this);
2287
2332
 
2288
2333
  if (!explicitNoTrack) {
@@ -2296,9 +2341,17 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2296
2341
  '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
2297
2342
  '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
2298
2343
  };
2344
+ _.each(captureExtraAttrs, function(attr) {
2345
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
2346
+ var attrVal = target.getAttribute(attr);
2347
+ if (shouldTrackValue(attrVal)) {
2348
+ props['$el_attr__' + attr] = attrVal;
2349
+ }
2350
+ }
2351
+ });
2299
2352
 
2300
2353
  if (captureTextContent) {
2301
- elementText = getSafeText(target);
2354
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2302
2355
  if (elementText && elementText.length) {
2303
2356
  props['$el_text'] = elementText;
2304
2357
  }
@@ -2314,14 +2367,22 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2314
2367
  }
2315
2368
  // prioritize text content from "real" click target if different from original target
2316
2369
  if (captureTextContent) {
2317
- var elementText = getSafeText(target);
2370
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2318
2371
  if (elementText && elementText.length) {
2319
2372
  props['$el_text'] = elementText;
2320
2373
  }
2321
2374
  }
2322
2375
 
2323
2376
  if (target) {
2324
- var targetProps = getPropertiesFromElement(target);
2377
+ // target may have been recalculated; check allowlists and blocklists again
2378
+ if (
2379
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
2380
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2381
+ ) {
2382
+ return null;
2383
+ }
2384
+
2385
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
2325
2386
  props['$target'] = targetProps;
2326
2387
  // pull up more props onto main event props
2327
2388
  props['$el_classes'] = targetProps['$classes'];
@@ -2337,19 +2398,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2337
2398
  }
2338
2399
 
2339
2400
 
2340
- /*
2401
+ /**
2341
2402
  * Get the direct text content of an element, protecting against sensitive data collection.
2342
2403
  * Concats textContent of each of the element's text node children; this avoids potential
2343
2404
  * collection of sensitive data that could happen if we used element.textContent and the
2344
2405
  * element had sensitive child elements, since element.textContent includes child content.
2345
2406
  * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
2346
2407
  * @param {Element} el - element to get the text of
2408
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2347
2409
  * @returns {string} the element's direct text content
2348
2410
  */
2349
- function getSafeText(el) {
2411
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
2350
2412
  var elText = '';
2351
2413
 
2352
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
2414
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
2353
2415
  _.each(el.childNodes, function(child) {
2354
2416
  if (isTextNode(child) && child.textContent) {
2355
2417
  elText += _.trim(child.textContent)
@@ -2388,6 +2450,75 @@ function guessRealClickTarget(ev) {
2388
2450
  return target;
2389
2451
  }
2390
2452
 
2453
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
2454
+ if (allowElementCallback) {
2455
+ try {
2456
+ if (!allowElementCallback(el, ev)) {
2457
+ return false;
2458
+ }
2459
+ } catch (err) {
2460
+ logger$3.critical('Error while checking element in allowElementCallback', err);
2461
+ return false;
2462
+ }
2463
+ }
2464
+
2465
+ if (!allowSelectors.length) {
2466
+ // no allowlist; all elements are fair game
2467
+ return true;
2468
+ }
2469
+
2470
+ for (var i = 0; i < allowSelectors.length; i++) {
2471
+ var sel = allowSelectors[i];
2472
+ try {
2473
+ if (el['matches'](sel)) {
2474
+ return true;
2475
+ }
2476
+ } catch (err) {
2477
+ logger$3.critical('Error while checking selector: ' + sel, err);
2478
+ }
2479
+ }
2480
+ return false;
2481
+ }
2482
+
2483
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
2484
+ var i;
2485
+
2486
+ if (blockElementCallback) {
2487
+ try {
2488
+ if (blockElementCallback(el, ev)) {
2489
+ return true;
2490
+ }
2491
+ } catch (err) {
2492
+ logger$3.critical('Error while checking element in blockElementCallback', err);
2493
+ return true;
2494
+ }
2495
+ }
2496
+
2497
+ if (blockSelectors && blockSelectors.length) {
2498
+ // programmatically prevent tracking of elements that match CSS selectors
2499
+ for (i = 0; i < blockSelectors.length; i++) {
2500
+ var sel = blockSelectors[i];
2501
+ try {
2502
+ if (el['matches'](sel)) {
2503
+ return true;
2504
+ }
2505
+ } catch (err) {
2506
+ logger$3.critical('Error while checking selector: ' + sel, err);
2507
+ }
2508
+ }
2509
+ }
2510
+
2511
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
2512
+ var classes = getClasses(el);
2513
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
2514
+ if (classes[OPT_OUT_CLASSES[i]]) {
2515
+ return true;
2516
+ }
2517
+ }
2518
+
2519
+ return false;
2520
+ }
2521
+
2391
2522
  /*
2392
2523
  * Check whether a DOM node has nodeType Node.ELEMENT_NODE
2393
2524
  * @param {Node} node - node to check
@@ -2462,11 +2593,16 @@ function shouldTrackDomEvent(el, ev) {
2462
2593
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2463
2594
  * using a variety of heuristics.
2464
2595
  * @param {Element} el - element to check
2596
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2465
2597
  * @returns {boolean} whether the element should be tracked
2466
2598
  */
2467
- function shouldTrackElement(el) {
2599
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
2468
2600
  var i;
2469
2601
 
2602
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
2603
+ return false;
2604
+ }
2605
+
2470
2606
  for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
2471
2607
  var classes = getClasses(curEl);
2472
2608
  for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
@@ -2556,9 +2692,17 @@ var PAGEVIEW_OPTION_FULL_URL = 'full-url';
2556
2692
  var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
2557
2693
  var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
2558
2694
 
2695
+ var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
2696
+ var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
2697
+ var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
2698
+ var CONFIG_BLOCK_ATTRS = 'block_attrs';
2699
+ var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
2559
2700
  var CONFIG_BLOCK_SELECTORS = 'block_selectors';
2560
2701
  var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
2702
+ var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
2561
2703
  var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
2704
+ var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
2705
+ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
2562
2706
  var CONFIG_TRACK_CLICK = 'click';
2563
2707
  var CONFIG_TRACK_INPUT = 'input';
2564
2708
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
@@ -2566,7 +2710,16 @@ var CONFIG_TRACK_SCROLL = 'scroll';
2566
2710
  var CONFIG_TRACK_SUBMIT = 'submit';
2567
2711
 
2568
2712
  var CONFIG_DEFAULTS = {};
2713
+ CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
2714
+ CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
2715
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
2716
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
2717
+ CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
2718
+ CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
2719
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
2569
2720
  CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
2721
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
2722
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
2570
2723
  CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
2571
2724
  CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
2572
2725
  CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
@@ -2621,13 +2774,37 @@ Autocapture.prototype.getConfig = function(key) {
2621
2774
  };
2622
2775
 
2623
2776
  Autocapture.prototype.currentUrlBlocked = function() {
2777
+ var i;
2778
+ var currentUrl = _.info.currentUrl();
2779
+
2780
+ var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
2781
+ if (allowUrlRegexes.length) {
2782
+ // we're using an allowlist, only track if current URL matches
2783
+ var allowed = false;
2784
+ for (i = 0; i < allowUrlRegexes.length; i++) {
2785
+ var allowRegex = allowUrlRegexes[i];
2786
+ try {
2787
+ if (currentUrl.match(allowRegex)) {
2788
+ allowed = true;
2789
+ break;
2790
+ }
2791
+ } catch (err) {
2792
+ logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
2793
+ return true;
2794
+ }
2795
+ }
2796
+ if (!allowed) {
2797
+ // wasn't allowed by any regex
2798
+ return true;
2799
+ }
2800
+ }
2801
+
2624
2802
  var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
2625
2803
  if (!blockUrlRegexes || !blockUrlRegexes.length) {
2626
2804
  return false;
2627
2805
  }
2628
2806
 
2629
- var currentUrl = _.info.currentUrl();
2630
- for (var i = 0; i < blockUrlRegexes.length; i++) {
2807
+ for (i = 0; i < blockUrlRegexes.length; i++) {
2631
2808
  try {
2632
2809
  if (currentUrl.match(blockUrlRegexes[i])) {
2633
2810
  return true;
@@ -2655,11 +2832,15 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
2655
2832
  return;
2656
2833
  }
2657
2834
 
2658
- var props = getPropsForDOMEvent(
2659
- ev,
2660
- this.getConfig(CONFIG_BLOCK_SELECTORS),
2661
- this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
2662
- );
2835
+ var props = getPropsForDOMEvent(ev, {
2836
+ allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
2837
+ allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
2838
+ blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
2839
+ blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
2840
+ blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
2841
+ captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
2842
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
2843
+ });
2663
2844
  if (props) {
2664
2845
  _.extend(props, DEFAULT_PROPS);
2665
2846
  this.mp.track(mpEventName, props);
@@ -2744,13 +2925,14 @@ Autocapture.prototype.initPageviewTracking = function() {
2744
2925
 
2745
2926
  var currentUrl = _.info.currentUrl();
2746
2927
  var shouldTrack = false;
2928
+ var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
2747
2929
  var trackPageviewOption = this.pageviewTrackingConfig();
2748
2930
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
2749
2931
  shouldTrack = currentUrl !== previousTrackedUrl;
2750
2932
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
2751
2933
  shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
2752
2934
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
2753
- shouldTrack = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
2935
+ shouldTrack = didPathChange;
2754
2936
  }
2755
2937
 
2756
2938
  if (shouldTrack) {
@@ -2758,6 +2940,10 @@ Autocapture.prototype.initPageviewTracking = function() {
2758
2940
  if (tracked) {
2759
2941
  previousTrackedUrl = currentUrl;
2760
2942
  }
2943
+ if (didPathChange) {
2944
+ this.lastScrollCheckpoint = 0;
2945
+ logger$3.log('Path change: re-initializing scroll depth checkpoints');
2946
+ }
2761
2947
  }
2762
2948
  }.bind(this)));
2763
2949
  };
@@ -2769,6 +2955,7 @@ Autocapture.prototype.initScrollTracking = function() {
2769
2955
  return;
2770
2956
  }
2771
2957
  logger$3.log('Initializing scroll tracking');
2958
+ this.lastScrollCheckpoint = 0;
2772
2959
 
2773
2960
  this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
2774
2961
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
@@ -2778,6 +2965,11 @@ Autocapture.prototype.initScrollTracking = function() {
2778
2965
  return;
2779
2966
  }
2780
2967
 
2968
+ var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
2969
+ var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
2970
+ .slice()
2971
+ .sort(function(a, b) { return a - b; });
2972
+
2781
2973
  var scrollTop = win.scrollY;
2782
2974
  var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
2783
2975
  try {
@@ -2785,10 +2977,25 @@ Autocapture.prototype.initScrollTracking = function() {
2785
2977
  var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
2786
2978
  props['$scroll_height'] = scrollHeight;
2787
2979
  props['$scroll_percentage'] = scrollPercentage;
2980
+ if (scrollPercentage > this.lastScrollCheckpoint) {
2981
+ for (var i = 0; i < scrollCheckpoints.length; i++) {
2982
+ var checkpoint = scrollCheckpoints[i];
2983
+ if (
2984
+ scrollPercentage >= checkpoint &&
2985
+ this.lastScrollCheckpoint < checkpoint
2986
+ ) {
2987
+ props['$scroll_checkpoint'] = checkpoint;
2988
+ this.lastScrollCheckpoint = checkpoint;
2989
+ shouldTrack = true;
2990
+ }
2991
+ }
2992
+ }
2788
2993
  } catch (err) {
2789
2994
  logger$3.critical('Error while calculating scroll percentage', err);
2790
2995
  }
2791
- this.mp.track(MP_EV_SCROLL, props);
2996
+ if (shouldTrack) {
2997
+ this.mp.track(MP_EV_SCROLL, props);
2998
+ }
2792
2999
  }.bind(this)));
2793
3000
  };
2794
3001
 
@@ -2988,7 +3195,7 @@ var SharedLock = function(key, options) {
2988
3195
  options = options || {};
2989
3196
 
2990
3197
  this.storageKey = key;
2991
- this.storage = options.storage || window.localStorage;
3198
+ this.storage = options.storage || win.localStorage;
2992
3199
  this.pollIntervalMS = options.pollIntervalMS || 100;
2993
3200
  this.timeoutMS = options.timeoutMS || 2000;
2994
3201
 
@@ -3003,7 +3210,6 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
3003
3210
  return new Promise(_.bind(function (resolve, reject) {
3004
3211
  var i = pid || (new Date().getTime() + '|' + Math.random());
3005
3212
  var startTime = new Date().getTime();
3006
-
3007
3213
  var key = this.storageKey;
3008
3214
  var pollIntervalMS = this.pollIntervalMS;
3009
3215
  var timeoutMS = this.timeoutMS;
@@ -3114,11 +3320,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
3114
3320
  };
3115
3321
 
3116
3322
  /**
3117
- * @typedef {import('./wrapper').StorageWrapper}
3118
- */
3119
-
3120
- /**
3121
- * @type {StorageWrapper}
3323
+ * @type {import('./wrapper').StorageWrapper}
3122
3324
  */
3123
3325
  var LocalStorageWrapper = function (storageOverride) {
3124
3326
  this.storage = storageOverride || localStorage;
@@ -3131,7 +3333,7 @@ LocalStorageWrapper.prototype.init = function () {
3131
3333
  LocalStorageWrapper.prototype.setItem = function (key, value) {
3132
3334
  return new PromisePolyfill(_.bind(function (resolve, reject) {
3133
3335
  try {
3134
- this.storage.setItem(key, value);
3336
+ this.storage.setItem(key, JSONStringify(value));
3135
3337
  } catch (e) {
3136
3338
  reject(e);
3137
3339
  }
@@ -3143,7 +3345,7 @@ LocalStorageWrapper.prototype.getItem = function (key) {
3143
3345
  return new PromisePolyfill(_.bind(function (resolve, reject) {
3144
3346
  var item;
3145
3347
  try {
3146
- item = this.storage.getItem(key);
3348
+ item = JSONParse(this.storage.getItem(key));
3147
3349
  } catch (e) {
3148
3350
  reject(e);
3149
3351
  }
@@ -3186,8 +3388,10 @@ var RequestQueue = function (storageKey, options) {
3186
3388
  this.usePersistence = options.usePersistence;
3187
3389
  if (this.usePersistence) {
3188
3390
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
3189
- this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
3190
- this.queueStorage.init();
3391
+ this.lock = new SharedLock(storageKey, {
3392
+ storage: options.sharedLockStorage || win.localStorage,
3393
+ timeoutMS: options.sharedLockTimeoutMS,
3394
+ });
3191
3395
  }
3192
3396
  this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
3193
3397
 
@@ -3195,6 +3399,14 @@ var RequestQueue = function (storageKey, options) {
3195
3399
 
3196
3400
  this.memQueue = [];
3197
3401
  this.initialized = false;
3402
+
3403
+ if (options.enqueueThrottleMs) {
3404
+ this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
3405
+ } else {
3406
+ this.enqueuePersisted = _.bind(function (queueEntry) {
3407
+ return this._enqueuePersisted([queueEntry]);
3408
+ }, this);
3409
+ }
3198
3410
  };
3199
3411
 
3200
3412
  RequestQueue.prototype.ensureInit = function () {
@@ -3237,36 +3449,39 @@ RequestQueue.prototype.enqueue = function (item, flushInterval) {
3237
3449
  this.memQueue.push(queueEntry);
3238
3450
  return PromisePolyfill.resolve(true);
3239
3451
  } else {
3452
+ return this.enqueuePersisted(queueEntry);
3453
+ }
3454
+ };
3240
3455
 
3241
- var enqueueItem = _.bind(function () {
3242
- return this.ensureInit()
3243
- .then(_.bind(function () {
3244
- return this.readFromStorage();
3245
- }, this))
3246
- .then(_.bind(function (storedQueue) {
3247
- storedQueue.push(queueEntry);
3248
- return this.saveToStorage(storedQueue);
3249
- }, this))
3250
- .then(_.bind(function (succeeded) {
3251
- // only add to in-memory queue when storage succeeds
3252
- if (succeeded) {
3253
- this.memQueue.push(queueEntry);
3254
- }
3255
- return succeeded;
3256
- }, this))
3257
- .catch(_.bind(function (err) {
3258
- this.reportError('Error enqueueing item', err, item);
3259
- return false;
3260
- }, this));
3261
- }, this);
3456
+ RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
3457
+ var enqueueItem = _.bind(function () {
3458
+ return this.ensureInit()
3459
+ .then(_.bind(function () {
3460
+ return this.readFromStorage();
3461
+ }, this))
3462
+ .then(_.bind(function (storedQueue) {
3463
+ return this.saveToStorage(storedQueue.concat(queueEntries));
3464
+ }, this))
3465
+ .then(_.bind(function (succeeded) {
3466
+ // only add to in-memory queue when storage succeeds
3467
+ if (succeeded) {
3468
+ this.memQueue = this.memQueue.concat(queueEntries);
3469
+ }
3262
3470
 
3263
- return this.lock
3264
- .withLock(enqueueItem, this.pid)
3471
+ return succeeded;
3472
+ }, this))
3265
3473
  .catch(_.bind(function (err) {
3266
- this.reportError('Error acquiring storage lock', err);
3474
+ this.reportError('Error enqueueing items', err, queueEntries);
3267
3475
  return false;
3268
3476
  }, this));
3269
- }
3477
+ }, this);
3478
+
3479
+ return this.lock
3480
+ .withLock(enqueueItem, this.pid)
3481
+ .catch(_.bind(function (err) {
3482
+ this.reportError('Error acquiring storage lock', err);
3483
+ return false;
3484
+ }, this));
3270
3485
  };
3271
3486
 
3272
3487
  /**
@@ -3287,7 +3502,7 @@ RequestQueue.prototype.fillBatch = function (batchSize) {
3287
3502
  }, this))
3288
3503
  .then(_.bind(function (storedQueue) {
3289
3504
  if (storedQueue.length) {
3290
- // item IDs already in batch; don't duplicate out of storage
3505
+ // item IDs already in batch; don't duplicate out of storage
3291
3506
  var idsInBatch = {}; // poor man's Set
3292
3507
  _.each(batch, function (item) {
3293
3508
  idsInBatch[item['id']] = true;
@@ -3374,7 +3589,7 @@ RequestQueue.prototype.removeItemsByID = function (ids) {
3374
3589
  .withLock(removeFromStorage, this.pid)
3375
3590
  .catch(_.bind(function (err) {
3376
3591
  this.reportError('Error acquiring storage lock', err);
3377
- if (!localStorageSupported(this.queueStorage.storage, true)) {
3592
+ if (!localStorageSupported(this.lock.storage, true)) {
3378
3593
  // Looks like localStorage writes have stopped working sometime after
3379
3594
  // initialization (probably full), and so nobody can acquire locks
3380
3595
  // anymore. Consider it temporarily safe to remove items without the
@@ -3462,7 +3677,6 @@ RequestQueue.prototype.readFromStorage = function () {
3462
3677
  }, this))
3463
3678
  .then(_.bind(function (storageEntry) {
3464
3679
  if (storageEntry) {
3465
- storageEntry = JSONParse(storageEntry);
3466
3680
  if (!_.isArray(storageEntry)) {
3467
3681
  this.reportError('Invalid storage entry:', storageEntry);
3468
3682
  storageEntry = null;
@@ -3480,16 +3694,9 @@ RequestQueue.prototype.readFromStorage = function () {
3480
3694
  * Serialize the given items array to localStorage.
3481
3695
  */
3482
3696
  RequestQueue.prototype.saveToStorage = function (queue) {
3483
- try {
3484
- var serialized = JSONStringify(queue);
3485
- } catch (err) {
3486
- this.reportError('Error serializing queue', err);
3487
- return PromisePolyfill.resolve(false);
3488
- }
3489
-
3490
3697
  return this.ensureInit()
3491
3698
  .then(_.bind(function () {
3492
- return this.queueStorage.setItem(this.storageKey, serialized);
3699
+ return this.queueStorage.setItem(this.storageKey, queue);
3493
3700
  }, this))
3494
3701
  .then(function () {
3495
3702
  return true;
@@ -3533,7 +3740,9 @@ var RequestBatcher = function(storageKey, options) {
3533
3740
  errorReporter: _.bind(this.reportError, this),
3534
3741
  queueStorage: options.queueStorage,
3535
3742
  sharedLockStorage: options.sharedLockStorage,
3536
- usePersistence: options.usePersistence
3743
+ sharedLockTimeoutMS: options.sharedLockTimeoutMS,
3744
+ usePersistence: options.usePersistence,
3745
+ enqueueThrottleMs: options.enqueueThrottleMs
3537
3746
  });
3538
3747
 
3539
3748
  this.libConfig = options.libConfig;
@@ -3555,6 +3764,8 @@ var RequestBatcher = function(storageKey, options) {
3555
3764
  // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
3556
3765
  // in a request loop and get ratelimited by the server.
3557
3766
  this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
3767
+
3768
+ this._flushPromise = null;
3558
3769
  };
3559
3770
 
3560
3771
  /**
@@ -3614,7 +3825,7 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
3614
3825
  if (!this.stopped) { // don't schedule anymore if batching has been stopped
3615
3826
  this.timeoutID = setTimeout(_.bind(function() {
3616
3827
  if (!this.stopped) {
3617
- this.flush();
3828
+ this._flushPromise = this.flush();
3618
3829
  }
3619
3830
  }, this), this.flushInterval);
3620
3831
  }
@@ -5240,8 +5451,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
5240
5451
  if (!(k in union_q)) {
5241
5452
  union_q[k] = [];
5242
5453
  }
5243
- // We may send duplicates, the server will dedup them.
5244
- union_q[k] = union_q[k].concat(v);
5454
+ // Prevent duplicate values
5455
+ _.each(v, function(item) {
5456
+ if (!_.include(union_q[k], item)) {
5457
+ union_q[k].push(item);
5458
+ }
5459
+ });
5245
5460
  }
5246
5461
  });
5247
5462
  this._pop_from_people_queue(UNSET_ACTION, q_data);
@@ -5326,6 +5541,129 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
5326
5541
  return timestamp;
5327
5542
  };
5328
5543
 
5544
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
5545
+
5546
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
5547
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
5548
+
5549
+ // note: increment the version number when adding new object stores
5550
+ var DB_VERSION = 1;
5551
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
5552
+
5553
+ /**
5554
+ * @type {import('./wrapper').StorageWrapper}
5555
+ */
5556
+ var IDBStorageWrapper = function (storeName) {
5557
+ /**
5558
+ * @type {Promise<IDBDatabase>|null}
5559
+ */
5560
+ this.dbPromise = null;
5561
+ this.storeName = storeName;
5562
+ };
5563
+
5564
+ IDBStorageWrapper.prototype._openDb = function () {
5565
+ return new PromisePolyfill(function (resolve, reject) {
5566
+ var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
5567
+ openRequest['onerror'] = function () {
5568
+ reject(openRequest.error);
5569
+ };
5570
+
5571
+ openRequest['onsuccess'] = function () {
5572
+ resolve(openRequest.result);
5573
+ };
5574
+
5575
+ openRequest['onupgradeneeded'] = function (ev) {
5576
+ var db = ev.target.result;
5577
+
5578
+ OBJECT_STORES.forEach(function (storeName) {
5579
+ db.createObjectStore(storeName);
5580
+ });
5581
+ };
5582
+ });
5583
+ };
5584
+
5585
+ IDBStorageWrapper.prototype.init = function () {
5586
+ if (!win.indexedDB) {
5587
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
5588
+ }
5589
+
5590
+ if (!this.dbPromise) {
5591
+ this.dbPromise = this._openDb();
5592
+ }
5593
+
5594
+ return this.dbPromise
5595
+ .then(function (dbOrError) {
5596
+ if (dbOrError instanceof win['IDBDatabase']) {
5597
+ return PromisePolyfill.resolve();
5598
+ } else {
5599
+ return PromisePolyfill.reject(dbOrError);
5600
+ }
5601
+ });
5602
+ };
5603
+
5604
+ /**
5605
+ * @param {IDBTransactionMode} mode
5606
+ * @param {function(IDBObjectStore): void} storeCb
5607
+ */
5608
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
5609
+ var storeName = this.storeName;
5610
+ var doTransaction = function (db) {
5611
+ return new PromisePolyfill(function (resolve, reject) {
5612
+ var transaction = db.transaction(storeName, mode);
5613
+ transaction.oncomplete = function () {
5614
+ resolve(transaction);
5615
+ };
5616
+ transaction.onabort = transaction.onerror = function () {
5617
+ reject(transaction.error);
5618
+ };
5619
+
5620
+ storeCb(transaction.objectStore(storeName));
5621
+ });
5622
+ };
5623
+
5624
+ return this.dbPromise
5625
+ .then(doTransaction)
5626
+ .catch(function (err) {
5627
+ if (err['name'] === 'InvalidStateError') {
5628
+ // try reopening the DB if the connection is closed
5629
+ this.dbPromise = this._openDb();
5630
+ return this.dbPromise.then(doTransaction);
5631
+ } else {
5632
+ return PromisePolyfill.reject(err);
5633
+ }
5634
+ }.bind(this));
5635
+ };
5636
+
5637
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
5638
+ return this.makeTransaction('readwrite', function (objectStore) {
5639
+ objectStore.put(value, key);
5640
+ });
5641
+ };
5642
+
5643
+ IDBStorageWrapper.prototype.getItem = function (key) {
5644
+ var req;
5645
+ return this.makeTransaction('readonly', function (objectStore) {
5646
+ req = objectStore.get(key);
5647
+ }).then(function () {
5648
+ return req.result;
5649
+ });
5650
+ };
5651
+
5652
+ IDBStorageWrapper.prototype.removeItem = function (key) {
5653
+ return this.makeTransaction('readwrite', function (objectStore) {
5654
+ objectStore.delete(key);
5655
+ });
5656
+ };
5657
+
5658
+ IDBStorageWrapper.prototype.getAll = function () {
5659
+ var req;
5660
+ return this.makeTransaction('readonly', function (objectStore) {
5661
+ req = objectStore.getAll();
5662
+ }).then(function () {
5663
+ return req.result;
5664
+ });
5665
+ };
5666
+
5329
5667
  /* eslint camelcase: "off" */
5330
5668
 
5331
5669
  /*
@@ -5340,11 +5678,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
5340
5678
  * Released under the MIT License.
5341
5679
  */
5342
5680
 
5343
- // ==ClosureCompiler==
5344
- // @compilation_level ADVANCED_OPTIMIZATIONS
5345
- // @output_file_name mixpanel-2.8.min.js
5346
- // ==/ClosureCompiler==
5347
-
5348
5681
  /*
5349
5682
  SIMPLE STYLE GUIDE:
5350
5683
 
@@ -5367,7 +5700,6 @@ var INIT_MODULE = 0;
5367
5700
  var INIT_SNIPPET = 1;
5368
5701
 
5369
5702
  var IDENTITY_FUNC = function(x) {return x;};
5370
- var NOOP_FUNC = function() {};
5371
5703
 
5372
5704
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
5373
5705
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
@@ -5676,34 +6008,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
5676
6008
  this.autocapture = new Autocapture(this);
5677
6009
  this.autocapture.init();
5678
6010
 
5679
- if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
5680
- this.start_session_recording();
6011
+ this._init_tab_id();
6012
+ this._check_and_start_session_recording();
6013
+ };
6014
+
6015
+ /**
6016
+ * Assigns a unique UUID to this tab / window by leveraging sessionStorage.
6017
+ * This is primarily used for session recording, where data must be isolated to the current tab.
6018
+ */
6019
+ MixpanelLib.prototype._init_tab_id = function() {
6020
+ if (_.sessionStorage.is_supported()) {
6021
+ try {
6022
+ var key_suffix = this.get_config('name') + '_' + this.get_config('token');
6023
+ var tab_id_key = 'mp_tab_id_' + key_suffix;
6024
+
6025
+ // A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
6026
+ // This enforces a unique ID in the cases like duplicated tab, window.open(...)
6027
+ var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
6028
+ if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
6029
+ _.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
6030
+ }
6031
+
6032
+ _.sessionStorage.set(should_generate_new_tab_id_key, '1');
6033
+ this.tab_id = _.sessionStorage.get(tab_id_key);
6034
+
6035
+ // Remove the flag when the tab is unloaded to indicate the stored tab ID can be reused. This event is not reliable to detect all page unloads,
6036
+ // but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
6037
+ // If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
6038
+ win.addEventListener('beforeunload', function () {
6039
+ _.sessionStorage.remove(should_generate_new_tab_id_key);
6040
+ });
6041
+ } catch(err) {
6042
+ this.report_error('Error initializing tab id', err);
6043
+ }
6044
+ } else {
6045
+ this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
5681
6046
  }
5682
6047
  };
5683
6048
 
5684
- MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () {
6049
+ MixpanelLib.prototype.get_tab_id = function () {
6050
+ return this.tab_id || null;
6051
+ };
6052
+
6053
+ MixpanelLib.prototype._should_load_recorder = function () {
6054
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
6055
+ var tab_id = this.get_tab_id();
6056
+ return recording_registry_idb.init()
6057
+ .then(function () {
6058
+ return recording_registry_idb.getAll();
6059
+ })
6060
+ .then(function (recordings) {
6061
+ for (var i = 0; i < recordings.length; i++) {
6062
+ // if there are expired recordings in the registry, we should load the recorder to flush them
6063
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
6064
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
6065
+ return true;
6066
+ }
6067
+ }
6068
+ return false;
6069
+ })
6070
+ .catch(_.bind(function (err) {
6071
+ this.report_error('Error checking recording registry', err);
6072
+ }, this));
6073
+ };
6074
+
6075
+ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
5685
6076
  if (!win['MutationObserver']) {
5686
6077
  console.critical('Browser does not support MutationObserver; skipping session recording');
5687
6078
  return;
5688
6079
  }
5689
6080
 
5690
- var handleLoadedRecorder = _.bind(function() {
5691
- this._recorder = this._recorder || new win['__mp_recorder'](this);
5692
- this._recorder['startRecording']();
6081
+ var loadRecorder = _.bind(function(startNewIfInactive) {
6082
+ var handleLoadedRecorder = _.bind(function() {
6083
+ this._recorder = this._recorder || new win['__mp_recorder'](this);
6084
+ this._recorder['resumeRecording'](startNewIfInactive);
6085
+ }, this);
6086
+
6087
+ if (_.isUndefined(win['__mp_recorder'])) {
6088
+ load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
6089
+ } else {
6090
+ handleLoadedRecorder();
6091
+ }
5693
6092
  }, this);
5694
6093
 
5695
- if (_.isUndefined(win['__mp_recorder'])) {
5696
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
6094
+ /**
6095
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
6096
+ * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
6097
+ */
6098
+ var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
6099
+ if (force_start || is_sampled) {
6100
+ loadRecorder(true);
5697
6101
  } else {
5698
- handleLoadedRecorder();
6102
+ this._should_load_recorder()
6103
+ .then(function (shouldLoad) {
6104
+ if (shouldLoad) {
6105
+ loadRecorder(false);
6106
+ }
6107
+ });
5699
6108
  }
5700
6109
  });
5701
6110
 
6111
+ MixpanelLib.prototype.start_session_recording = function () {
6112
+ this._check_and_start_session_recording(true);
6113
+ };
6114
+
5702
6115
  MixpanelLib.prototype.stop_session_recording = function () {
5703
6116
  if (this._recorder) {
5704
6117
  this._recorder['stopRecording']();
5705
- } else {
5706
- console.critical('Session recorder module not loaded');
6118
+ }
6119
+ };
6120
+
6121
+ MixpanelLib.prototype.pause_session_recording = function () {
6122
+ if (this._recorder) {
6123
+ this._recorder['pauseRecording']();
6124
+ }
6125
+ };
6126
+
6127
+ MixpanelLib.prototype.resume_session_recording = function () {
6128
+ if (this._recorder) {
6129
+ this._recorder['resumeRecording']();
5707
6130
  }
5708
6131
  };
5709
6132
 
@@ -5738,6 +6161,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
5738
6161
  return replay_id || null;
5739
6162
  };
5740
6163
 
6164
+ // "private" public method to reach into the recorder in test cases
6165
+ MixpanelLib.prototype.__get_recorder = function () {
6166
+ return this._recorder;
6167
+ };
6168
+
5741
6169
  // Private methods
5742
6170
 
5743
6171
  MixpanelLib.prototype._loaded = function() {
@@ -6077,7 +6505,8 @@ MixpanelLib.prototype.init_batchers = function() {
6077
6505
  return this._run_hook('before_send_' + attrs.type, item);
6078
6506
  }, this),
6079
6507
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
6080
- usePersistence: true
6508
+ usePersistence: true,
6509
+ enqueueThrottleMs: 10,
6081
6510
  }
6082
6511
  );
6083
6512
  }, this);
@@ -7178,6 +7607,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
7178
7607
 
7179
7608
  if (disabled) {
7180
7609
  this.stop_batch_senders();
7610
+ this.stop_session_recording();
7181
7611
  } else {
7182
7612
  // only start batchers after opt-in if they have previously been started
7183
7613
  // in order to avoid unintentionally starting up batching for the first time
@@ -7418,10 +7848,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
7418
7848
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
7419
7849
  MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
7420
7850
  MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
7851
+ MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
7852
+ MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
7421
7853
  MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
7422
7854
  MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
7855
+ MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
7423
7856
  MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
7424
7857
 
7858
+ // Exports intended only for testing
7859
+ MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
7860
+
7425
7861
  // MixpanelPersistence Exports
7426
7862
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
7427
7863
  MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;