mixpanel-browser 2.69.1 → 2.71.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/build.sh +1 -0
  3. package/dist/mixpanel-core.cjs.d.ts +440 -0
  4. package/dist/mixpanel-core.cjs.js +857 -56
  5. package/dist/mixpanel-recorder.js +5 -3
  6. package/dist/mixpanel-recorder.min.js +1 -1
  7. package/dist/mixpanel-recorder.min.js.map +1 -1
  8. package/dist/mixpanel-with-async-recorder.cjs.d.ts +440 -0
  9. package/dist/mixpanel-with-async-recorder.cjs.js +857 -56
  10. package/dist/mixpanel-with-recorder.d.ts +440 -0
  11. package/dist/mixpanel-with-recorder.js +861 -58
  12. package/dist/mixpanel-with-recorder.min.d.ts +440 -0
  13. package/dist/mixpanel-with-recorder.min.js +1 -1
  14. package/dist/mixpanel.amd.d.ts +440 -0
  15. package/dist/mixpanel.amd.js +861 -58
  16. package/dist/mixpanel.cjs.d.ts +440 -0
  17. package/dist/mixpanel.cjs.js +861 -58
  18. package/dist/mixpanel.globals.js +857 -56
  19. package/dist/mixpanel.min.js +170 -152
  20. package/dist/mixpanel.module.d.ts +440 -0
  21. package/dist/mixpanel.module.js +861 -58
  22. package/dist/mixpanel.umd.d.ts +440 -0
  23. package/dist/mixpanel.umd.js +861 -58
  24. package/dist/rrweb-compiled.js +4 -2
  25. package/package.json +2 -19
  26. package/rollup.config.mjs +28 -4
  27. package/src/autocapture/deadclick.js +254 -0
  28. package/src/autocapture/index.js +237 -41
  29. package/src/autocapture/shadow-dom-observer.js +100 -0
  30. package/src/autocapture/utils.js +230 -3
  31. package/src/config.js +1 -1
  32. package/src/flags/index.js +43 -18
  33. package/src/index.d.ts +16 -3
  34. package/src/loaders/loader-module-core.d.ts +1 -0
  35. package/src/loaders/loader-module-with-async-recorder.d.ts +1 -0
  36. package/src/loaders/loader-module.d.ts +1 -0
  37. package/src/utils.js +15 -0
@@ -1595,7 +1595,8 @@ function snapshot(n2, options) {
1595
1595
  week: true,
1596
1596
  textarea: true,
1597
1597
  select: true,
1598
- password: true
1598
+ password: true,
1599
+ hidden: true
1599
1600
  } : maskAllInputs === false ? {
1600
1601
  password: true
1601
1602
  } : maskAllInputs;
@@ -13237,7 +13238,8 @@ function record(options) {
13237
13238
  week: true,
13238
13239
  textarea: true,
13239
13240
  select: true,
13240
- password: true
13241
+ password: true,
13242
+ hidden: true
13241
13243
  } : _maskInputOptions !== void 0 ? _maskInputOptions : {
13242
13244
  password: true
13243
13245
  };
@@ -14041,7 +14043,7 @@ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]'
14041
14043
 
14042
14044
  var Config = {
14043
14045
  DEBUG: false,
14044
- LIB_VERSION: '2.69.0'
14046
+ LIB_VERSION: '2.71.0'
14045
14047
  };
14046
14048
 
14047
14049
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -15720,6 +15722,20 @@ var cheap_guid = function(maxlen) {
15720
15722
  return maxlen ? guid.substring(0, maxlen) : guid;
15721
15723
  };
15722
15724
 
15725
+ /**
15726
+ * Generates a W3C traceparent header for easy interop with distributed tracing systems i.e Open Telemetry
15727
+ * https://www.w3.org/TR/trace-context/#traceparent-header
15728
+ */
15729
+ var generateTraceparent = function() {
15730
+ var traceID = _.UUID().replace(/-/g, '');
15731
+ var parentID = _.UUID().replace(/-/g, '').substring(0, 16);
15732
+
15733
+ // Sampled trace
15734
+ var traceFlags = '01';
15735
+
15736
+ return '00-' + traceID + '-' + parentID + '-' + traceFlags;
15737
+ };
15738
+
15723
15739
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
15724
15740
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
15725
15741
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -17855,11 +17871,17 @@ win['__mp_recorder'] = MixpanelRecorder;
17855
17871
  var EV_CHANGE = 'change';
17856
17872
  var EV_CLICK = 'click';
17857
17873
  var EV_HASHCHANGE = 'hashchange';
17874
+ var EV_INPUT = 'input';
17875
+ var EV_LOAD = 'load';
17858
17876
  var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
17859
17877
  var EV_POPSTATE = 'popstate';
17860
17878
  // TODO scrollend isn't available in Safari: document or polyfill?
17861
17879
  var EV_SCROLLEND = 'scrollend';
17880
+ var EV_SCROLL = 'scroll';
17881
+ var EV_SELECT = 'select';
17862
17882
  var EV_SUBMIT = 'submit';
17883
+ var EV_TOGGLE = 'toggle';
17884
+ var EV_VISIBILITYCHANGE = 'visibilitychange';
17863
17885
 
17864
17886
  var CLICK_EVENT_PROPS = [
17865
17887
  'clientX', 'clientY',
@@ -17876,6 +17898,77 @@ var TRACKED_ATTRS = [
17876
17898
  'href', 'name', 'role', 'title', 'type'
17877
17899
  ];
17878
17900
 
17901
+ var INTERACTIVE_ARIA_ROLES = {
17902
+ 'button': true,
17903
+ 'checkbox': true,
17904
+ 'combobox': true,
17905
+ 'grid': true,
17906
+ 'link': true,
17907
+ 'listbox': true,
17908
+ 'menu': true,
17909
+ 'menubar': true,
17910
+ 'menuitem': true,
17911
+ 'menuitemcheckbox': true,
17912
+ 'menuitemradio': true,
17913
+ 'navigation': true,
17914
+ 'option': true,
17915
+ 'radio': true,
17916
+ 'radiogroup': true,
17917
+ 'searchbox': true,
17918
+ 'slider': true,
17919
+ 'spinbutton': true,
17920
+ 'switch': true,
17921
+ 'tab': true,
17922
+ 'tablist': true,
17923
+ 'textbox': true,
17924
+ 'tree': true,
17925
+ 'treegrid': true,
17926
+ 'treeitem': true
17927
+ };
17928
+
17929
+ var ALWAYS_NON_INTERACTIVE_TAGS = {
17930
+ // Document metadata
17931
+ 'base': true,
17932
+ 'head': true,
17933
+ 'html': true,
17934
+ 'link': true,
17935
+ 'meta': true,
17936
+ 'script': true,
17937
+ 'style': true,
17938
+ 'title': true,
17939
+ // Text formatting
17940
+ 'br': true,
17941
+ 'hr': true,
17942
+ 'wbr': true,
17943
+ // Other
17944
+ 'noscript': true,
17945
+ 'picture': true,
17946
+ 'source': true,
17947
+ 'template': true,
17948
+ 'track': true
17949
+ };
17950
+
17951
+ // Common container tags that need additional checks
17952
+ var TEXT_CONTAINER_TAGS = {
17953
+ 'article': true,
17954
+ 'div': true,
17955
+ 'h1': true,
17956
+ 'h2': true,
17957
+ 'h3': true,
17958
+ 'h4': true,
17959
+ 'h5': true,
17960
+ 'h6': true,
17961
+ 'p': true,
17962
+ 'section': true,
17963
+ 'span': true
17964
+ };
17965
+
17966
+ var EVENT_HANDLER_ATTRIBUTES = [
17967
+ 'onclick', 'onmousedown', 'onmouseup', 'onpointerdown', 'onpointerup', 'ontouchend', 'ontouchstart'
17968
+ ];
17969
+
17970
+ var MAX_DEPTH = 5;
17971
+
17879
17972
  var logger$1 = console_with_prefix('autocapture');
17880
17973
 
17881
17974
 
@@ -18243,6 +18336,10 @@ function minDOMApisSupported() {
18243
18336
  }
18244
18337
  }
18245
18338
 
18339
+ function weakSetSupported() {
18340
+ return typeof WeakSet !== 'undefined';
18341
+ }
18342
+
18246
18343
  /*
18247
18344
  * Check whether a DOM event should be "tracked" or if it may contain sensitive data
18248
18345
  * using a variety of heuristics.
@@ -18368,6 +18465,149 @@ function shouldTrackValue(value) {
18368
18465
  return true;
18369
18466
  }
18370
18467
 
18468
+ /**
18469
+ * Creates a cross-browser compatible scroll end function with appropriate event listener.
18470
+ * For browsers that support scrollend, returns the original function with scrollend event.
18471
+ * For browsers without scrollend support, returns a debounced function that triggers
18472
+ * 100ms after the last scroll event to simulate scrollend behavior.
18473
+ * @param {Function} originalFunction - The function to call when scrolling ends
18474
+ * @returns {Object} Object containing listener function and eventType string
18475
+ * @returns {Function} returns.listener - The wrapped function to use as event listener
18476
+ * @returns {string} returns.eventType - The event type to listen for ('scrollend' or 'scroll')
18477
+ */
18478
+ function getPolyfillScrollEndFunction(originalFunction) {
18479
+ var supportsScrollEnd = 'onscrollend' in win;
18480
+ var polyfillFunction = safewrap(originalFunction);
18481
+ var polyfillEvent = EV_SCROLLEND;
18482
+ if (!supportsScrollEnd) {
18483
+ // Polyfill for browsers without scrollend support: wait 100ms after the last scroll event
18484
+ // https://developer.chrome.com/blog/scrollend-a-new-javascript-event
18485
+ var scrollTimer = null;
18486
+ var scrollDelayMs = 100;
18487
+
18488
+ polyfillFunction = safewrap(function() {
18489
+ clearTimeout(scrollTimer);
18490
+ scrollTimer = setTimeout(originalFunction, scrollDelayMs);
18491
+ });
18492
+
18493
+ polyfillEvent = EV_SCROLL;
18494
+ }
18495
+
18496
+ return {
18497
+ listener: polyfillFunction,
18498
+ eventType: polyfillEvent
18499
+ };
18500
+ }
18501
+
18502
+ function hasInlineEventHandlers(element) {
18503
+ for (var i = 0; i < EVENT_HANDLER_ATTRIBUTES.length; i++) {
18504
+ if (element.hasAttribute(EVENT_HANDLER_ATTRIBUTES[i])) {
18505
+ return true;
18506
+ }
18507
+ }
18508
+ return false;
18509
+ }
18510
+
18511
+ function hasInteractiveAriaRole(element) {
18512
+ var role = element.getAttribute('role');
18513
+ if (!role) return false;
18514
+
18515
+ // Handle invalid markup where multiple roles might be specified
18516
+ // Only the first token is recognized per ARIA spec
18517
+ var primaryRole = role.trim().split(/\s+/)[0].toLowerCase();
18518
+
18519
+ return INTERACTIVE_ARIA_ROLES[primaryRole];
18520
+ }
18521
+
18522
+ function hasAnyInteractivityIndicators(element) {
18523
+ var tagName = element.tagName.toLowerCase();
18524
+
18525
+ // Check for interactive HTML elements
18526
+ if (tagName === 'button' ||
18527
+ tagName === 'input' ||
18528
+ tagName === 'select' ||
18529
+ tagName === 'textarea' ||
18530
+ tagName === 'details' ||
18531
+ tagName === 'dialog') {
18532
+ return true;
18533
+ }
18534
+
18535
+ if (element.isContentEditable) {
18536
+ return true;
18537
+ }
18538
+
18539
+ if (element.onclick || element.onmousedown || element.onmouseup || element.ontouchstart || element.ontouchend) {
18540
+ return true;
18541
+ }
18542
+
18543
+ if (hasInlineEventHandlers(element)) {
18544
+ return true;
18545
+ }
18546
+
18547
+ if (hasInteractiveAriaRole(element)) {
18548
+ return true;
18549
+ }
18550
+
18551
+ if (tagName === 'a' && element.hasAttribute('href')) {
18552
+ return true;
18553
+ }
18554
+
18555
+ if (element.hasAttribute('tabindex')) {
18556
+ return true;
18557
+ }
18558
+
18559
+ return false;
18560
+ }
18561
+
18562
+
18563
+ function isDefinitelyNonInteractive(element) {
18564
+ if (!element || !element.tagName) {
18565
+ return true;
18566
+ }
18567
+
18568
+ var tagName = element.tagName.toLowerCase();
18569
+
18570
+ // These tags are definitely non-interactive
18571
+ if (ALWAYS_NON_INTERACTIVE_TAGS[tagName]) {
18572
+ return true;
18573
+ }
18574
+
18575
+ // For all other elements, we can only be certain they're non-interactive if they lack ALL indicators of interactivity
18576
+ // Check for any signs of interactivity
18577
+ if (hasAnyInteractivityIndicators(element)) {
18578
+ return false;
18579
+ }
18580
+
18581
+ // Check parent chain for interactive context
18582
+ var parent = element.parentElement;
18583
+ var depth = 0;
18584
+
18585
+ while (parent && depth < MAX_DEPTH) {
18586
+ if (hasAnyInteractivityIndicators(parent)) {
18587
+ return false; // Element is inside an interactive parent
18588
+ }
18589
+
18590
+ if (parent.getRootNode && parent.getRootNode() !== document$1) {
18591
+ var root = parent.getRootNode();
18592
+ if (root.host && hasAnyInteractivityIndicators(root.host)) {
18593
+ return false; // Inside an interactive shadow host
18594
+ }
18595
+ }
18596
+
18597
+ parent = parent.parentElement;
18598
+ depth++;
18599
+ }
18600
+
18601
+ // Pure text containers without any interactive context
18602
+ if (TEXT_CONTAINER_TAGS[tagName]) {
18603
+ // These are non-interactive ONLY if they have no interactive indicators (already checked as part of hasAnyInteractivityIndicators)
18604
+ return true;
18605
+ }
18606
+
18607
+ // Default: we can't be certain it's non-interactive
18608
+ return false;
18609
+ }
18610
+
18371
18611
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
18372
18612
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
18373
18613
  /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
@@ -18400,6 +18640,350 @@ RageClickTracker.prototype.isRageClick = function(x, y, options) {
18400
18640
  return false;
18401
18641
  };
18402
18642
 
18643
+ function ShadowDOMObserver(changeCallback, observerConfig) {
18644
+ this.changeCallback = changeCallback || function() {};
18645
+ this.observerConfig = observerConfig;
18646
+
18647
+ this.observedShadowRoots = null;
18648
+ this.shadowObservers = [];
18649
+ }
18650
+
18651
+ ShadowDOMObserver.prototype.getEventTarget = function(event) {
18652
+ if (!this.observedShadowRoots) {
18653
+ return;
18654
+ }
18655
+ var path = this.getComposedPath(event);
18656
+ if (path && path.length) {
18657
+ return path[0];
18658
+ }
18659
+
18660
+ return event['target'] || event['srcElement'];
18661
+ };
18662
+
18663
+
18664
+ ShadowDOMObserver.prototype.getComposedPath = function(event) {
18665
+ if ('composedPath' in event) {
18666
+ return event['composedPath']();
18667
+ }
18668
+
18669
+ return [];
18670
+ };
18671
+ ShadowDOMObserver.prototype.observeFromEvent = function(event) {
18672
+ if (!this.observedShadowRoots) {
18673
+ return;
18674
+ }
18675
+
18676
+ var path = this.getComposedPath(event);
18677
+
18678
+ // Check each element in path for shadow roots
18679
+ for (var i = 0; i < path.length; i++) {
18680
+ var element = path[i];
18681
+
18682
+ if (element && element.shadowRoot) {
18683
+ this.observeShadowRoot(element.shadowRoot);
18684
+ }
18685
+ }
18686
+ };
18687
+
18688
+
18689
+ ShadowDOMObserver.prototype.observeShadowRoot = function(shadowRoot) {
18690
+ if (!this.observedShadowRoots || this.observedShadowRoots.has(shadowRoot)) {
18691
+ return;
18692
+ }
18693
+
18694
+ var self = this;
18695
+
18696
+ try {
18697
+ this.observedShadowRoots.add(shadowRoot);
18698
+
18699
+ var observer = new window.MutationObserver(function() {
18700
+ self.changeCallback();
18701
+ });
18702
+
18703
+ observer.observe(shadowRoot, this.observerConfig);
18704
+ this.shadowObservers.push(observer);
18705
+ } catch (e) {
18706
+ logger$1.critical('Error while observing shadow root', e);
18707
+ }
18708
+ };
18709
+
18710
+
18711
+ ShadowDOMObserver.prototype.start = function() {
18712
+ if (this.observedShadowRoots) {
18713
+ return;
18714
+ }
18715
+
18716
+ if (!weakSetSupported()) {
18717
+ logger$1.critical('Shadow DOM observation unavailable: WeakSet not supported');
18718
+ return;
18719
+ }
18720
+
18721
+ this.observedShadowRoots = new WeakSet();
18722
+ };
18723
+
18724
+ ShadowDOMObserver.prototype.stop = function() {
18725
+ if (!this.observedShadowRoots) {
18726
+ return;
18727
+ }
18728
+
18729
+ for (var i = 0; i < this.shadowObservers.length; i++) {
18730
+ try {
18731
+ this.shadowObservers[i].disconnect();
18732
+ } catch (e) {
18733
+ logger$1.critical('Error while disconnecting shadow DOM observer', e);
18734
+ }
18735
+ }
18736
+ this.shadowObservers = [];
18737
+ this.observedShadowRoots = null;
18738
+ };
18739
+
18740
+ /** @const */ var DEFAULT_DEAD_CLICK_TIMEOUT_MS = 500;
18741
+ /** @const */ var INTERACTION_EVENTS = [EV_CHANGE, EV_INPUT, EV_SUBMIT, EV_SELECT, EV_TOGGLE];
18742
+ /** @const */ var LAYOUT_EVENTS = [EV_SCROLLEND];
18743
+ /** @const */ var NAVIGATION_EVENTS = [EV_HASHCHANGE];
18744
+ /** @const */ var MUTATION_OBSERVER_CONFIG = {
18745
+ characterData: true,
18746
+ childList: true,
18747
+ subtree: true,
18748
+ attributes: true,
18749
+ attributeFilter: ['style', 'class', 'hidden', 'checked', 'selected', 'value', 'display', 'visibility']
18750
+ };
18751
+
18752
+
18753
+ function DeadClickTracker(onDeadClickCallback) {
18754
+ this.eventListeners = [];
18755
+ this.mutationObserver = null;
18756
+ this.shadowDOMObserver = null;
18757
+
18758
+ this.isTracking = false;
18759
+ this.lastChangeEventTimestamp = 0;
18760
+ this.pendingClicks = [];
18761
+ this.onDeadClickCallback = onDeadClickCallback;
18762
+ this.processingActive = false;
18763
+ this.processingTimeout = null;
18764
+ }
18765
+
18766
+
18767
+ DeadClickTracker.prototype.addClick = function(event) {
18768
+ var element = this.shadowDOMObserver && this.shadowDOMObserver.getEventTarget(event);
18769
+
18770
+ if (!element) {
18771
+ element = event['target'] || event['srcElement'];
18772
+ }
18773
+
18774
+ if (!element || isDefinitelyNonInteractive(element)) {
18775
+ return false;
18776
+ }
18777
+
18778
+ if (this.shadowDOMObserver) {
18779
+ this.shadowDOMObserver.observeFromEvent(event);
18780
+ }
18781
+ this.pendingClicks.push({
18782
+ element: element,
18783
+ event: event,
18784
+ timestamp: Date.now()
18785
+ });
18786
+ return true;
18787
+ };
18788
+
18789
+ DeadClickTracker.prototype.trackClick = function(event, config) {
18790
+ if (!this.isTracking) {
18791
+ return false;
18792
+ }
18793
+
18794
+ var added = this.addClick(event);
18795
+ if (added) {
18796
+ this.triggerProcessing(config);
18797
+ }
18798
+ return added;
18799
+ };
18800
+
18801
+ DeadClickTracker.prototype.getDeadClicks = function(config) {
18802
+ if (this.pendingClicks.length === 0) {
18803
+ return [];
18804
+ }
18805
+
18806
+ var timeoutMs = config['timeout_ms'];
18807
+ var now = Date.now();
18808
+ var clicksToEvaluate = this.pendingClicks.slice(); // Copy array
18809
+ this.pendingClicks = []; // Clear original
18810
+
18811
+ var deadClicks = [];
18812
+
18813
+ for (var i = 0; i < clicksToEvaluate.length; i++) {
18814
+ var click = clicksToEvaluate[i];
18815
+
18816
+ if (now - click.timestamp >= timeoutMs) {
18817
+ // Click has exceeded timeout, check if it's dead by looking for changes after this specific click
18818
+ if (!this.hasChangesAfter(click.timestamp)) {
18819
+ deadClicks.push(click);
18820
+ }
18821
+ } else {
18822
+ // Still pending - add back
18823
+ this.pendingClicks.push(click);
18824
+ }
18825
+ }
18826
+
18827
+ return deadClicks;
18828
+ };
18829
+
18830
+ DeadClickTracker.prototype.hasChangesAfter = function(timestamp) {
18831
+ // 100ms tolerance for race condition between when we record the click and the change event
18832
+ return this.lastChangeEventTimestamp >= (timestamp - 100);
18833
+ };
18834
+
18835
+ DeadClickTracker.prototype.recordChangeEvent = function() {
18836
+ this.lastChangeEventTimestamp = Date.now();
18837
+ };
18838
+
18839
+ DeadClickTracker.prototype.triggerProcessing = function(config) {
18840
+ // Prevent multiple concurrent processing chains
18841
+ if (this.processingActive) {
18842
+ return;
18843
+ }
18844
+ this.processingActive = true;
18845
+ this.processRecursively(config);
18846
+ };
18847
+
18848
+ DeadClickTracker.prototype.processRecursively = function(config) {
18849
+ if (!this.isTracking || !this.onDeadClickCallback) {
18850
+ this.processingActive = false;
18851
+ return;
18852
+ }
18853
+
18854
+ var timeoutMs = config['timeout_ms'];
18855
+ var self = this;
18856
+
18857
+ this.processingTimeout = setTimeout(function() {
18858
+ if (!self.processingActive) {
18859
+ return;
18860
+ }
18861
+
18862
+ var deadClicks = self.getDeadClicks(config);
18863
+
18864
+ for (var i = 0; i < deadClicks.length; i++) {
18865
+ self.onDeadClickCallback(deadClicks[i].event);
18866
+ }
18867
+
18868
+ if (self.pendingClicks.length > 0) {
18869
+ self.processRecursively(config);
18870
+ } else {
18871
+ self.processingActive = false;
18872
+ }
18873
+ }, timeoutMs);
18874
+ };
18875
+
18876
+ DeadClickTracker.prototype.startTracking = function() {
18877
+ if (this.isTracking) {
18878
+ return;
18879
+ }
18880
+
18881
+ this.isTracking = true;
18882
+
18883
+ var self = this;
18884
+
18885
+ INTERACTION_EVENTS.forEach(function(event) {
18886
+ var handler = function() {
18887
+ self.recordChangeEvent();
18888
+ };
18889
+ document.addEventListener(event, handler, { capture: true, passive: true });
18890
+ self.eventListeners.push({ target: document, event: event, handler: handler, options: { capture: true, passive: true } });
18891
+ });
18892
+ NAVIGATION_EVENTS.forEach(function(event) {
18893
+ var handler = function() {
18894
+ self.recordChangeEvent();
18895
+ };
18896
+ window.addEventListener(event, handler);
18897
+ self.eventListeners.push({ target: window, event: event, handler: handler });
18898
+ });
18899
+ LAYOUT_EVENTS.forEach(function(event) {
18900
+ var handler = function() {
18901
+ self.recordChangeEvent();
18902
+ };
18903
+ window.addEventListener(event, handler, { passive: true });
18904
+ self.eventListeners.push({ target: window, event: event, handler: handler, options: { passive: true } });
18905
+ });
18906
+ var selectionHandler = function() {
18907
+ self.recordChangeEvent();
18908
+ };
18909
+ document.addEventListener('selectionchange', selectionHandler);
18910
+ self.eventListeners.push({ target: document, event: 'selectionchange', handler: selectionHandler });
18911
+
18912
+ // Set up MutationObserver
18913
+ if (window.MutationObserver) {
18914
+ try {
18915
+ this.mutationObserver = new window.MutationObserver(function() {
18916
+ self.recordChangeEvent();
18917
+ });
18918
+
18919
+ this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
18920
+ } catch (e) {
18921
+ logger$1.critical('Error while setting up mutation observer', e);
18922
+ }
18923
+ }
18924
+
18925
+ // Set up Shadow DOM observer
18926
+ if (window.customElements) {
18927
+ try {
18928
+ this.shadowDOMObserver = new ShadowDOMObserver(
18929
+ function() {
18930
+ self.recordChangeEvent();
18931
+ },
18932
+ MUTATION_OBSERVER_CONFIG
18933
+ );
18934
+ this.shadowDOMObserver.start();
18935
+ } catch (e) {
18936
+ logger$1.critical('Error while setting up shadow DOM observer', e);
18937
+ this.shadowDOMObserver = null;
18938
+ }
18939
+ }
18940
+ };
18941
+
18942
+ DeadClickTracker.prototype.stopTracking = function() {
18943
+ if (!this.isTracking) {
18944
+ return;
18945
+ }
18946
+
18947
+ this.isTracking = false;
18948
+ this.pendingClicks = [];
18949
+ this.lastChangeEventTimestamp = 0;
18950
+ this.processingActive = false;
18951
+
18952
+ if (this.processingTimeout) {
18953
+ clearTimeout(this.processingTimeout);
18954
+ this.processingTimeout = null;
18955
+ }
18956
+
18957
+ // Remove all event listeners
18958
+ for (var i = 0; i < this.eventListeners.length; i++) {
18959
+ var listener = this.eventListeners[i];
18960
+ try {
18961
+ listener.target.removeEventListener(listener.event, listener.handler, listener.options);
18962
+ } catch (e) {
18963
+ logger$1.critical('Error while removing event listener', e);
18964
+ }
18965
+ }
18966
+ this.eventListeners = [];
18967
+
18968
+ if (this.mutationObserver) {
18969
+ try {
18970
+ this.mutationObserver.disconnect();
18971
+ } catch (e) {
18972
+ logger$1.critical('Error while disconnecting mutation observer', e);
18973
+ }
18974
+ this.mutationObserver = null;
18975
+ }
18976
+
18977
+ if (this.shadowDOMObserver) {
18978
+ try {
18979
+ this.shadowDOMObserver.stop();
18980
+ } catch (e) {
18981
+ logger$1.critical('Error while stopping shadow DOM observer', e);
18982
+ }
18983
+ this.shadowDOMObserver = null;
18984
+ }
18985
+ };
18986
+
18403
18987
  var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
18404
18988
  var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
18405
18989
 
@@ -18419,10 +19003,12 @@ var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
18419
19003
  var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
18420
19004
  var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
18421
19005
  var CONFIG_TRACK_CLICK = 'click';
19006
+ var CONFIG_TRACK_DEAD_CLICK = 'dead_click';
18422
19007
  var CONFIG_TRACK_INPUT = 'input';
18423
19008
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
18424
19009
  var CONFIG_TRACK_RAGE_CLICK = 'rage_click';
18425
19010
  var CONFIG_TRACK_SCROLL = 'scroll';
19011
+ var CONFIG_TRACK_PAGE_LEAVE = 'page_leave';
18426
19012
  var CONFIG_TRACK_SUBMIT = 'submit';
18427
19013
 
18428
19014
  var CONFIG_DEFAULTS$1 = {};
@@ -18437,10 +19023,12 @@ CONFIG_DEFAULTS$1[CONFIG_CAPTURE_TEXT_CONTENT] = false;
18437
19023
  CONFIG_DEFAULTS$1[CONFIG_SCROLL_CAPTURE_ALL] = false;
18438
19024
  CONFIG_DEFAULTS$1[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
18439
19025
  CONFIG_DEFAULTS$1[CONFIG_TRACK_CLICK] = true;
19026
+ CONFIG_DEFAULTS$1[CONFIG_TRACK_DEAD_CLICK] = true;
18440
19027
  CONFIG_DEFAULTS$1[CONFIG_TRACK_INPUT] = true;
18441
19028
  CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
18442
19029
  CONFIG_DEFAULTS$1[CONFIG_TRACK_RAGE_CLICK] = true;
18443
19030
  CONFIG_DEFAULTS$1[CONFIG_TRACK_SCROLL] = true;
19031
+ CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGE_LEAVE] = false;
18444
19032
  CONFIG_DEFAULTS$1[CONFIG_TRACK_SUBMIT] = true;
18445
19033
 
18446
19034
  var DEFAULT_PROPS = {
@@ -18448,10 +19036,12 @@ var DEFAULT_PROPS = {
18448
19036
  };
18449
19037
 
18450
19038
  var MP_EV_CLICK = '$mp_click';
19039
+ var MP_EV_DEAD_CLICK = '$mp_dead_click';
18451
19040
  var MP_EV_INPUT = '$mp_input_change';
18452
19041
  var MP_EV_RAGE_CLICK = '$mp_rage_click';
18453
19042
  var MP_EV_SCROLL = '$mp_scroll';
18454
19043
  var MP_EV_SUBMIT = '$mp_submit';
19044
+ var MP_EV_PAGE_LEAVE = '$mp_page_leave';
18455
19045
 
18456
19046
  /**
18457
19047
  * Autocapture: manages automatic event tracking
@@ -18459,6 +19049,9 @@ var MP_EV_SUBMIT = '$mp_submit';
18459
19049
  */
18460
19050
  var Autocapture = function(mp) {
18461
19051
  this.mp = mp;
19052
+ this.maxScrollViewDepth = 0;
19053
+ this.hasTrackedScrollSession = false;
19054
+ this.previousScrollHeight = 0;
18462
19055
  };
18463
19056
 
18464
19057
  Autocapture.prototype.init = function() {
@@ -18466,13 +19059,15 @@ Autocapture.prototype.init = function() {
18466
19059
  logger$1.critical('Autocapture unavailable: missing required DOM APIs');
18467
19060
  return;
18468
19061
  }
18469
-
19062
+ this.initPageListeners();
18470
19063
  this.initPageviewTracking();
18471
19064
  this.initClickTracking();
19065
+ this.initDeadClickTracking();
18472
19066
  this.initInputTracking();
18473
19067
  this.initScrollTracking();
18474
19068
  this.initSubmitTracking();
18475
19069
  this.initRageClickTracking();
19070
+ this.initPageLeaveTracking();
18476
19071
  };
18477
19072
 
18478
19073
  Autocapture.prototype.getFullConfig = function() {
@@ -18553,7 +19148,8 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
18553
19148
 
18554
19149
  var isCapturedForHeatMap = this.mp.is_recording_heatmap_data() && (
18555
19150
  (mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK)) ||
18556
- (mpEventName === MP_EV_RAGE_CLICK && !this._getRageClickConfig())
19151
+ (mpEventName === MP_EV_RAGE_CLICK && !this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK)) ||
19152
+ (mpEventName === MP_EV_DEAD_CLICK && !this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK))
18557
19153
  );
18558
19154
 
18559
19155
  var props = getPropsForDOMEvent(ev, {
@@ -18572,11 +19168,45 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
18572
19168
  }
18573
19169
  };
18574
19170
 
18575
- Autocapture.prototype._getRageClickConfig = function() {
18576
- var config = this.getConfig(CONFIG_TRACK_RAGE_CLICK);
19171
+ Autocapture.prototype.initPageListeners = function() {
19172
+ win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
19173
+ win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
19174
+
19175
+ if (!this.pageviewTrackingConfig() && !this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.get_config('record_heatmap_data')) {
19176
+ // These are all the configs that use these listeners
19177
+ return;
19178
+ }
19179
+
19180
+ this.listenerPopstate = function() {
19181
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
19182
+ };
19183
+ this.listenerHashchange = function() {
19184
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
19185
+ };
19186
+
19187
+ win.addEventListener(EV_POPSTATE, this.listenerPopstate);
19188
+ win.addEventListener(EV_HASHCHANGE, this.listenerHashchange);
19189
+ var nativePushState = win.history.pushState;
19190
+ if (typeof nativePushState === 'function') {
19191
+ win.history.pushState = function(state, unused, url) {
19192
+ nativePushState.call(win.history, state, unused, url);
19193
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
19194
+ };
19195
+ }
19196
+ var nativeReplaceState = win.history.replaceState;
19197
+ if (typeof nativeReplaceState === 'function') {
19198
+ win.history.replaceState = function(state, unused, url) {
19199
+ nativeReplaceState.call(win.history, state, unused, url);
19200
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
19201
+ };
19202
+ }
19203
+ };
19204
+
19205
+ Autocapture.prototype._getClickTrackingConfig = function(configKey) {
19206
+ var config = this.getConfig(configKey);
18577
19207
 
18578
19208
  if (!config) {
18579
- return null; // rage click tracking disabled
19209
+ return null; // click tracking disabled
18580
19210
  }
18581
19211
 
18582
19212
  if (config === true) {
@@ -18590,6 +19220,68 @@ Autocapture.prototype._getRageClickConfig = function() {
18590
19220
  return {}; // fallback to defaults for any other truthy value
18591
19221
  };
18592
19222
 
19223
+ Autocapture.prototype._trackPageLeave = function(ev, currentUrl, currentScrollHeight) {
19224
+ if (this.hasTrackedScrollSession) {
19225
+ // User has navigated away already ending their impression.
19226
+ return;
19227
+ }
19228
+ this.hasTrackedScrollSession = true;
19229
+ var viewportHeight = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
19230
+ var scrollPercentage = Math.round(Math.max(this.maxScrollViewDepth - viewportHeight, 0) / (currentScrollHeight - viewportHeight) * 100);
19231
+ var foldLinePercentage = Math.round((viewportHeight / currentScrollHeight) * 100);
19232
+ if (currentScrollHeight <= viewportHeight) {
19233
+ // If the content fits within the viewport, consider it fully scrolled
19234
+ scrollPercentage = 100;
19235
+ foldLinePercentage = 100;
19236
+ }
19237
+
19238
+ var props = _.extend({
19239
+ '$max_scroll_view_depth': this.maxScrollViewDepth,
19240
+ '$max_scroll_percentage': scrollPercentage,
19241
+ '$fold_line_percentage': foldLinePercentage,
19242
+ '$scroll_height': currentScrollHeight,
19243
+ '$event_type': ev.type,
19244
+ '$current_url': currentUrl || _.info.currentUrl(),
19245
+ '$viewportHeight': viewportHeight, // This is the fold line
19246
+ '$viewportWidth': Math.max(document$1.documentElement.clientWidth, win.innerWidth || 0),
19247
+ }, DEFAULT_PROPS);
19248
+
19249
+ if (this.mp.is_recording_heatmap_data() && !this.getConfig(CONFIG_TRACK_PAGE_LEAVE)) {
19250
+ props['$captured_for_heatmap'] = true;
19251
+ }
19252
+
19253
+ // Send with beacon transport to ensure event is sent before unload
19254
+ this.mp.track(MP_EV_PAGE_LEAVE, props, {transport: 'sendBeacon'});
19255
+ };
19256
+
19257
+ Autocapture.prototype._initScrollDepthTracking = function() {
19258
+ win.removeEventListener(EV_SCROLL, this.listenerScrollDepth);
19259
+ win.removeEventListener(EV_SCROLLEND, this.listenerScrollDepth);
19260
+
19261
+ if (!this.mp.get_config('record_heatmap_data')) {
19262
+ return;
19263
+ }
19264
+
19265
+ logger$1.log('Initializing scroll depth tracking');
19266
+
19267
+ this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
19268
+
19269
+ var updateScrollDepth = function() {
19270
+ if (this.currentUrlBlocked()) {
19271
+ return;
19272
+ }
19273
+ var scrollViewHeight = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0) + win.scrollY;
19274
+ if (scrollViewHeight > this.maxScrollViewDepth) {
19275
+ this.maxScrollViewDepth = scrollViewHeight;
19276
+ }
19277
+ this.previousScrollHeight = document$1.body.scrollHeight;
19278
+ }.bind(this);
19279
+
19280
+ var scrollEndPolyfill = getPolyfillScrollEndFunction(updateScrollDepth);
19281
+ this.listenerScrollDepth = scrollEndPolyfill.listener;
19282
+ win.addEventListener(scrollEndPolyfill.eventType, this.listenerScrollDepth);
19283
+ };
19284
+
18593
19285
  Autocapture.prototype.initClickTracking = function() {
18594
19286
  win.removeEventListener(EV_CLICK, this.listenerClick);
18595
19287
 
@@ -18598,12 +19290,49 @@ Autocapture.prototype.initClickTracking = function() {
18598
19290
  }
18599
19291
  logger$1.log('Initializing click tracking');
18600
19292
 
18601
- this.listenerClick = win.addEventListener(EV_CLICK, function(ev) {
19293
+ this.listenerClick = function(ev) {
18602
19294
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
18603
19295
  return;
18604
19296
  }
18605
19297
  this.trackDomEvent(ev, MP_EV_CLICK);
18606
- }.bind(this));
19298
+ }.bind(this);
19299
+ win.addEventListener(EV_CLICK, this.listenerClick);
19300
+ };
19301
+
19302
+ Autocapture.prototype.initDeadClickTracking = function() {
19303
+ var deadClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK);
19304
+
19305
+ if (!deadClickConfig && !this.mp.get_config('record_heatmap_data')) {
19306
+ this.stopDeadClickTracking();
19307
+ return;
19308
+ }
19309
+
19310
+ logger$1.log('Initializing dead click tracking');
19311
+ if (!this._deadClickTracker) {
19312
+ this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
19313
+ this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
19314
+ }.bind(this));
19315
+ this._deadClickTracker.startTracking();
19316
+ }
19317
+
19318
+ if (!this.listenerDeadClick) {
19319
+ this.listenerDeadClick = function(ev) {
19320
+ var currentDeadClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK);
19321
+ if (!currentDeadClickConfig && !this.mp.is_recording_heatmap_data()) {
19322
+ return;
19323
+ }
19324
+ if (this.currentUrlBlocked()) {
19325
+ return;
19326
+ }
19327
+ // Normalize config to ensure timeout_ms is always set
19328
+ var normalizedConfig = currentDeadClickConfig || {};
19329
+ if (!normalizedConfig['timeout_ms']) {
19330
+ normalizedConfig['timeout_ms'] = DEFAULT_DEAD_CLICK_TIMEOUT_MS;
19331
+ }
19332
+ this._deadClickTracker.trackClick(ev, normalizedConfig);
19333
+ }.bind(this);
19334
+ win.addEventListener(EV_CLICK, this.listenerDeadClick);
19335
+ }
18607
19336
  };
18608
19337
 
18609
19338
  Autocapture.prototype.initInputTracking = function() {
@@ -18614,17 +19343,16 @@ Autocapture.prototype.initInputTracking = function() {
18614
19343
  }
18615
19344
  logger$1.log('Initializing input tracking');
18616
19345
 
18617
- this.listenerChange = win.addEventListener(EV_CHANGE, function(ev) {
19346
+ this.listenerChange = function(ev) {
18618
19347
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
18619
19348
  return;
18620
19349
  }
18621
19350
  this.trackDomEvent(ev, MP_EV_INPUT);
18622
- }.bind(this));
19351
+ }.bind(this);
19352
+ win.addEventListener(EV_CHANGE, this.listenerChange);
18623
19353
  };
18624
19354
 
18625
19355
  Autocapture.prototype.initPageviewTracking = function() {
18626
- win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
18627
- win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
18628
19356
  win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
18629
19357
 
18630
19358
  if (!this.pageviewTrackingConfig()) {
@@ -18641,27 +19369,7 @@ Autocapture.prototype.initPageviewTracking = function() {
18641
19369
  previousTrackedUrl = _.info.currentUrl();
18642
19370
  }
18643
19371
 
18644
- this.listenerPopstate = win.addEventListener(EV_POPSTATE, function() {
18645
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
18646
- });
18647
- this.listenerHashchange = win.addEventListener(EV_HASHCHANGE, function() {
18648
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
18649
- });
18650
- var nativePushState = win.history.pushState;
18651
- if (typeof nativePushState === 'function') {
18652
- win.history.pushState = function(state, unused, url) {
18653
- nativePushState.call(win.history, state, unused, url);
18654
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
18655
- };
18656
- }
18657
- var nativeReplaceState = win.history.replaceState;
18658
- if (typeof nativeReplaceState === 'function') {
18659
- win.history.replaceState = function(state, unused, url) {
18660
- nativeReplaceState.call(win.history, state, unused, url);
18661
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
18662
- };
18663
- }
18664
- this.listenerLocationchange = win.addEventListener(EV_MP_LOCATION_CHANGE, safewrap(function() {
19372
+ this.listenerLocationchange = safewrap(function() {
18665
19373
  if (this.currentUrlBlocked()) {
18666
19374
  return;
18667
19375
  }
@@ -18688,13 +19396,14 @@ Autocapture.prototype.initPageviewTracking = function() {
18688
19396
  logger$1.log('Path change: re-initializing scroll depth checkpoints');
18689
19397
  }
18690
19398
  }
18691
- }.bind(this)));
19399
+ }.bind(this));
19400
+ win.addEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
18692
19401
  };
18693
19402
 
18694
19403
  Autocapture.prototype.initRageClickTracking = function() {
18695
19404
  win.removeEventListener(EV_CLICK, this.listenerRageClick);
18696
19405
 
18697
- var rageClickConfig = this._getRageClickConfig();
19406
+ var rageClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK);
18698
19407
  if (!rageClickConfig && !this.mp.get_config('record_heatmap_data')) {
18699
19408
  return;
18700
19409
  }
@@ -18705,7 +19414,7 @@ Autocapture.prototype.initRageClickTracking = function() {
18705
19414
  }
18706
19415
 
18707
19416
  this.listenerRageClick = function(ev) {
18708
- var currentRageClickConfig = this._getRageClickConfig();
19417
+ var currentRageClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK);
18709
19418
  if (!currentRageClickConfig && !this.mp.is_recording_heatmap_data()) {
18710
19419
  return;
18711
19420
  }
@@ -18723,6 +19432,8 @@ Autocapture.prototype.initRageClickTracking = function() {
18723
19432
 
18724
19433
  Autocapture.prototype.initScrollTracking = function() {
18725
19434
  win.removeEventListener(EV_SCROLLEND, this.listenerScroll);
19435
+ win.removeEventListener(EV_SCROLL, this.listenerScroll);
19436
+
18726
19437
 
18727
19438
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
18728
19439
  return;
@@ -18730,7 +19441,7 @@ Autocapture.prototype.initScrollTracking = function() {
18730
19441
  logger$1.log('Initializing scroll tracking');
18731
19442
  this.lastScrollCheckpoint = 0;
18732
19443
 
18733
- this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
19444
+ var scrollTrackFunction = function() {
18734
19445
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
18735
19446
  return;
18736
19447
  }
@@ -18769,7 +19480,11 @@ Autocapture.prototype.initScrollTracking = function() {
18769
19480
  if (shouldTrack) {
18770
19481
  this.mp.track(MP_EV_SCROLL, props);
18771
19482
  }
18772
- }.bind(this)));
19483
+ }.bind(this);
19484
+
19485
+ var scrollEndPolyfill = getPolyfillScrollEndFunction(scrollTrackFunction);
19486
+ this.listenerScroll = scrollEndPolyfill.listener;
19487
+ win.addEventListener(scrollEndPolyfill.eventType, this.listenerScroll);
18773
19488
  };
18774
19489
 
18775
19490
  Autocapture.prototype.initSubmitTracking = function() {
@@ -18780,18 +19495,81 @@ Autocapture.prototype.initSubmitTracking = function() {
18780
19495
  }
18781
19496
  logger$1.log('Initializing submit tracking');
18782
19497
 
18783
- this.listenerSubmit = win.addEventListener(EV_SUBMIT, function(ev) {
19498
+ this.listenerSubmit = function(ev) {
18784
19499
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
18785
19500
  return;
18786
19501
  }
18787
19502
  this.trackDomEvent(ev, MP_EV_SUBMIT);
19503
+ }.bind(this);
19504
+ win.addEventListener(EV_SUBMIT, this.listenerSubmit);
19505
+ };
19506
+
19507
+ Autocapture.prototype.initPageLeaveTracking = function() {
19508
+ // Capture page_leave both when the user navigates away from the page (visibilitychange) as well
19509
+ // as when they navigate to a different page within the SPA (popstate/pushstate/hashchange).
19510
+ document$1.removeEventListener(EV_VISIBILITYCHANGE, this.listenerPageLeaveVisibilitychange);
19511
+ win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerPageLeaveLocationchange);
19512
+ win.removeEventListener(EV_LOAD, this.listenerPageLoad);
19513
+
19514
+ if (!this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.get_config('record_heatmap_data')) {
19515
+ return;
19516
+ }
19517
+
19518
+ logger$1.log('Initializing page visibility tracking.');
19519
+ this._initScrollDepthTracking();
19520
+ var previousTrackedUrl = _.info.currentUrl();
19521
+
19522
+ // Initialize previousScrollHeight on `load` which handles async loading
19523
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
19524
+ this.listenerPageLoad = function() {
19525
+ this.previousScrollHeight = document$1.body.scrollHeight;
19526
+ }.bind(this);
19527
+ win.addEventListener(EV_LOAD, this.listenerPageLoad);
19528
+
19529
+ // Track page navigation events similar to how initPageviewTracking does it
19530
+ this.listenerPageLeaveLocationchange = safewrap(function(ev) {
19531
+ if (this.currentUrlBlocked()) {
19532
+ return;
19533
+ }
19534
+
19535
+ var currentUrl = _.info.currentUrl();
19536
+ // Track all URL changes including query string or fragment changes as separate scroll sessions
19537
+ var shouldTrack = currentUrl !== previousTrackedUrl;
19538
+
19539
+ if (shouldTrack) {
19540
+ this._trackPageLeave(ev, previousTrackedUrl, this.previousScrollHeight);
19541
+ previousTrackedUrl = currentUrl;
19542
+ // Fragment navigation should call scroll(end) and trigger listener, don't add window.scrollY here.
19543
+ this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
19544
+ this.previousScrollHeight = document$1.body.scrollHeight;
19545
+ this.hasTrackedScrollSession = false;
19546
+ }
18788
19547
  }.bind(this));
19548
+ win.addEventListener(EV_MP_LOCATION_CHANGE, this.listenerPageLeaveLocationchange);
19549
+
19550
+ this.listenerPageLeaveVisibilitychange = function(ev) {
19551
+ if (document$1.hidden) {
19552
+ this._trackPageLeave(ev, previousTrackedUrl, this.previousScrollHeight);
19553
+ }
19554
+ }.bind(this);
19555
+ document$1.addEventListener(EV_VISIBILITYCHANGE, this.listenerPageLeaveVisibilitychange);
19556
+ };
19557
+
19558
+ Autocapture.prototype.stopDeadClickTracking = function() {
19559
+ if (this.listenerDeadClick) {
19560
+ win.removeEventListener(EV_CLICK, this.listenerDeadClick);
19561
+ this.listenerDeadClick = null;
19562
+ }
19563
+
19564
+ if (this._deadClickTracker) {
19565
+ this._deadClickTracker.stopTracking();
19566
+ this._deadClickTracker = null;
19567
+ }
18789
19568
  };
18790
19569
 
18791
19570
  // TODO integrate error_reporter from mixpanel instance
18792
19571
  safewrapClass(Autocapture);
18793
19572
 
18794
- var fetch = win['fetch'];
18795
19573
  var logger = console_with_prefix('flags');
18796
19574
 
18797
19575
  var FLAGS_CONFIG_KEY = 'flags';
@@ -18805,6 +19583,7 @@ CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
18805
19583
  * @constructor
18806
19584
  */
18807
19585
  var FeatureFlagManager = function(initOptions) {
19586
+ this.fetch = win['fetch'];
18808
19587
  this.getFullApiRoute = initOptions.getFullApiRoute;
18809
19588
  this.getMpConfig = initOptions.getConfigFunc;
18810
19589
  this.setMpConfig = initOptions.setConfigFunc;
@@ -18813,7 +19592,7 @@ var FeatureFlagManager = function(initOptions) {
18813
19592
  };
18814
19593
 
18815
19594
  FeatureFlagManager.prototype.init = function() {
18816
- if (!minApisSupported()) {
19595
+ if (!this.minApisSupported()) {
18817
19596
  logger.critical('Feature Flags unavailable: missing minimum required APIs');
18818
19597
  return;
18819
19598
  }
@@ -18876,18 +19655,24 @@ FeatureFlagManager.prototype.fetchFlags = function() {
18876
19655
 
18877
19656
  var distinctId = this.getMpProperty('distinct_id');
18878
19657
  var deviceId = this.getMpProperty('$device_id');
19658
+ var traceparent = generateTraceparent();
18879
19659
  logger.log('Fetching flags for distinct ID: ' + distinctId);
18880
- var reqParams = {
18881
- 'context': _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT))
18882
- };
19660
+
19661
+ var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
19662
+ var searchParams = new URLSearchParams();
19663
+ searchParams.set('context', JSON.stringify(context));
19664
+ searchParams.set('token', this.getMpConfig('token'));
19665
+ searchParams.set('mp_lib', 'web');
19666
+ searchParams.set('$lib_version', Config.LIB_VERSION);
19667
+ var url = this.getFullApiRoute() + '?' + searchParams.toString();
19668
+
18883
19669
  this._fetchInProgressStartTime = Date.now();
18884
- this.fetchPromise = win['fetch'](this.getFullApiRoute(), {
18885
- 'method': 'POST',
19670
+ this.fetchPromise = this.fetch.call(win, url, {
19671
+ 'method': 'GET',
18886
19672
  'headers': {
18887
19673
  'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
18888
- 'Content-Type': 'application/octet-stream'
18889
- },
18890
- 'body': JSON.stringify(reqParams)
19674
+ 'traceparent': traceparent
19675
+ }
18891
19676
  }).then(function(response) {
18892
19677
  this.markFetchComplete();
18893
19678
  return response.json().then(function(responseBody) {
@@ -18899,10 +19684,14 @@ FeatureFlagManager.prototype.fetchFlags = function() {
18899
19684
  _.each(responseFlags, function(data, key) {
18900
19685
  flags.set(key, {
18901
19686
  'key': data['variant_key'],
18902
- 'value': data['variant_value']
19687
+ 'value': data['variant_value'],
19688
+ 'experiment_id': data['experiment_id'],
19689
+ 'is_experiment_active': data['is_experiment_active'],
19690
+ 'is_qa_tester': data['is_qa_tester']
18903
19691
  });
18904
19692
  });
18905
19693
  this.flags = flags;
19694
+ this._traceparent = traceparent;
18906
19695
  }.bind(this)).catch(function(error) {
18907
19696
  this.markFetchComplete();
18908
19697
  logger.error(error);
@@ -18999,22 +19788,36 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
18999
19788
  return;
19000
19789
  }
19001
19790
  this.trackedFeatures.add(featureName);
19002
- this.track('$experiment_started', {
19791
+
19792
+ var trackingProperties = {
19003
19793
  'Experiment name': featureName,
19004
19794
  'Variant name': feature['key'],
19005
19795
  '$experiment_type': 'feature_flag',
19006
19796
  'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
19007
19797
  'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
19008
- 'Variant fetch latency (ms)': this._fetchLatency
19009
- });
19798
+ 'Variant fetch latency (ms)': this._fetchLatency,
19799
+ 'Variant fetch traceparent': this._traceparent,
19800
+ };
19801
+
19802
+ if (feature['experiment_id'] !== 'undefined') {
19803
+ trackingProperties['$experiment_id'] = feature['experiment_id'];
19804
+ }
19805
+ if (feature['is_experiment_active'] !== 'undefined') {
19806
+ trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
19807
+ }
19808
+ if (feature['is_qa_tester'] !== 'undefined') {
19809
+ trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
19810
+ }
19811
+
19812
+ this.track('$experiment_started', trackingProperties);
19010
19813
  };
19011
19814
 
19012
- function minApisSupported() {
19013
- return !!fetch &&
19815
+ FeatureFlagManager.prototype.minApisSupported = function() {
19816
+ return !!this.fetch &&
19014
19817
  typeof Promise !== 'undefined' &&
19015
19818
  typeof Map !== 'undefined' &&
19016
19819
  typeof Set !== 'undefined';
19017
- }
19820
+ };
19018
19821
 
19019
19822
  safewrapClass(FeatureFlagManager);
19020
19823