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