mixpanel-browser 2.70.0 → 2.71.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/CHANGELOG.md +11 -0
  3. package/build.sh +1 -0
  4. package/dist/mixpanel-core.cjs.d.ts +440 -0
  5. package/dist/mixpanel-core.cjs.js +849 -50
  6. package/dist/mixpanel-recorder.js +5 -3
  7. package/dist/mixpanel-recorder.min.js +1 -1
  8. package/dist/mixpanel-recorder.min.js.map +1 -1
  9. package/dist/mixpanel-with-async-recorder.cjs.d.ts +440 -0
  10. package/dist/mixpanel-with-async-recorder.cjs.js +849 -50
  11. package/dist/mixpanel-with-recorder.d.ts +440 -0
  12. package/dist/mixpanel-with-recorder.js +853 -52
  13. package/dist/mixpanel-with-recorder.min.d.ts +440 -0
  14. package/dist/mixpanel-with-recorder.min.js +1 -1
  15. package/dist/mixpanel.amd.d.ts +440 -0
  16. package/dist/mixpanel.amd.js +853 -52
  17. package/dist/mixpanel.cjs.d.ts +440 -0
  18. package/dist/mixpanel.cjs.js +853 -52
  19. package/dist/mixpanel.globals.js +849 -50
  20. package/dist/mixpanel.min.js +170 -153
  21. package/dist/mixpanel.module.d.ts +440 -0
  22. package/dist/mixpanel.module.js +853 -52
  23. package/dist/mixpanel.umd.d.ts +440 -0
  24. package/dist/mixpanel.umd.js +853 -52
  25. package/dist/rrweb-compiled.js +4 -2
  26. package/package.json +2 -19
  27. package/rollup.config.mjs +28 -4
  28. package/src/autocapture/deadclick.js +254 -0
  29. package/src/autocapture/index.js +239 -41
  30. package/src/autocapture/shadow-dom-observer.js +100 -0
  31. package/src/autocapture/utils.js +230 -3
  32. package/src/config.js +1 -1
  33. package/src/flags/index.js +32 -12
  34. package/src/index.d.ts +16 -3
  35. package/src/loaders/loader-module-core.d.ts +1 -0
  36. package/src/loaders/loader-module-with-async-recorder.d.ts +1 -0
  37. package/src/loaders/loader-module.d.ts +1 -0
  38. package/src/utils.js +15 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.70.0'
5
+ LIB_VERSION: '2.71.1'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -2064,6 +2064,20 @@ var cheap_guid = function(maxlen) {
2064
2064
  return maxlen ? guid.substring(0, maxlen) : guid;
2065
2065
  };
2066
2066
 
2067
+ /**
2068
+ * Generates a W3C traceparent header for easy interop with distributed tracing systems i.e Open Telemetry
2069
+ * https://www.w3.org/TR/trace-context/#traceparent-header
2070
+ */
2071
+ var generateTraceparent = function() {
2072
+ var traceID = _.UUID().replace(/-/g, '');
2073
+ var parentID = _.UUID().replace(/-/g, '').substring(0, 16);
2074
+
2075
+ // Sampled trace
2076
+ var traceFlags = '01';
2077
+
2078
+ return '00-' + traceID + '-' + parentID + '-' + traceFlags;
2079
+ };
2080
+
2067
2081
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
2068
2082
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
2069
2083
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -2141,11 +2155,17 @@ var isRecordingExpired = function(serializedRecording) {
2141
2155
  var EV_CHANGE = 'change';
2142
2156
  var EV_CLICK = 'click';
2143
2157
  var EV_HASHCHANGE = 'hashchange';
2158
+ var EV_INPUT = 'input';
2159
+ var EV_LOAD = 'load';
2144
2160
  var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
2145
2161
  var EV_POPSTATE = 'popstate';
2146
2162
  // TODO scrollend isn't available in Safari: document or polyfill?
2147
2163
  var EV_SCROLLEND = 'scrollend';
2164
+ var EV_SCROLL = 'scroll';
2165
+ var EV_SELECT = 'select';
2148
2166
  var EV_SUBMIT = 'submit';
2167
+ var EV_TOGGLE = 'toggle';
2168
+ var EV_VISIBILITYCHANGE = 'visibilitychange';
2149
2169
 
2150
2170
  var CLICK_EVENT_PROPS = [
2151
2171
  'clientX', 'clientY',
@@ -2162,6 +2182,77 @@ var TRACKED_ATTRS = [
2162
2182
  'href', 'name', 'role', 'title', 'type'
2163
2183
  ];
2164
2184
 
2185
+ var INTERACTIVE_ARIA_ROLES = {
2186
+ 'button': true,
2187
+ 'checkbox': true,
2188
+ 'combobox': true,
2189
+ 'grid': true,
2190
+ 'link': true,
2191
+ 'listbox': true,
2192
+ 'menu': true,
2193
+ 'menubar': true,
2194
+ 'menuitem': true,
2195
+ 'menuitemcheckbox': true,
2196
+ 'menuitemradio': true,
2197
+ 'navigation': true,
2198
+ 'option': true,
2199
+ 'radio': true,
2200
+ 'radiogroup': true,
2201
+ 'searchbox': true,
2202
+ 'slider': true,
2203
+ 'spinbutton': true,
2204
+ 'switch': true,
2205
+ 'tab': true,
2206
+ 'tablist': true,
2207
+ 'textbox': true,
2208
+ 'tree': true,
2209
+ 'treegrid': true,
2210
+ 'treeitem': true
2211
+ };
2212
+
2213
+ var ALWAYS_NON_INTERACTIVE_TAGS = {
2214
+ // Document metadata
2215
+ 'base': true,
2216
+ 'head': true,
2217
+ 'html': true,
2218
+ 'link': true,
2219
+ 'meta': true,
2220
+ 'script': true,
2221
+ 'style': true,
2222
+ 'title': true,
2223
+ // Text formatting
2224
+ 'br': true,
2225
+ 'hr': true,
2226
+ 'wbr': true,
2227
+ // Other
2228
+ 'noscript': true,
2229
+ 'picture': true,
2230
+ 'source': true,
2231
+ 'template': true,
2232
+ 'track': true
2233
+ };
2234
+
2235
+ // Common container tags that need additional checks
2236
+ var TEXT_CONTAINER_TAGS = {
2237
+ 'article': true,
2238
+ 'div': true,
2239
+ 'h1': true,
2240
+ 'h2': true,
2241
+ 'h3': true,
2242
+ 'h4': true,
2243
+ 'h5': true,
2244
+ 'h6': true,
2245
+ 'p': true,
2246
+ 'section': true,
2247
+ 'span': true
2248
+ };
2249
+
2250
+ var EVENT_HANDLER_ATTRIBUTES = [
2251
+ 'onclick', 'onmousedown', 'onmouseup', 'onpointerdown', 'onpointerup', 'ontouchend', 'ontouchstart'
2252
+ ];
2253
+
2254
+ var MAX_DEPTH = 5;
2255
+
2165
2256
  var logger$4 = console_with_prefix('autocapture');
2166
2257
 
2167
2258
 
@@ -2529,6 +2620,10 @@ function minDOMApisSupported() {
2529
2620
  }
2530
2621
  }
2531
2622
 
2623
+ function weakSetSupported() {
2624
+ return typeof WeakSet !== 'undefined';
2625
+ }
2626
+
2532
2627
  /*
2533
2628
  * Check whether a DOM event should be "tracked" or if it may contain sensitive data
2534
2629
  * using a variety of heuristics.
@@ -2654,6 +2749,149 @@ function shouldTrackValue(value) {
2654
2749
  return true;
2655
2750
  }
2656
2751
 
2752
+ /**
2753
+ * Creates a cross-browser compatible scroll end function with appropriate event listener.
2754
+ * For browsers that support scrollend, returns the original function with scrollend event.
2755
+ * For browsers without scrollend support, returns a debounced function that triggers
2756
+ * 100ms after the last scroll event to simulate scrollend behavior.
2757
+ * @param {Function} originalFunction - The function to call when scrolling ends
2758
+ * @returns {Object} Object containing listener function and eventType string
2759
+ * @returns {Function} returns.listener - The wrapped function to use as event listener
2760
+ * @returns {string} returns.eventType - The event type to listen for ('scrollend' or 'scroll')
2761
+ */
2762
+ function getPolyfillScrollEndFunction(originalFunction) {
2763
+ var supportsScrollEnd = 'onscrollend' in win;
2764
+ var polyfillFunction = safewrap(originalFunction);
2765
+ var polyfillEvent = EV_SCROLLEND;
2766
+ if (!supportsScrollEnd) {
2767
+ // Polyfill for browsers without scrollend support: wait 100ms after the last scroll event
2768
+ // https://developer.chrome.com/blog/scrollend-a-new-javascript-event
2769
+ var scrollTimer = null;
2770
+ var scrollDelayMs = 100;
2771
+
2772
+ polyfillFunction = safewrap(function() {
2773
+ clearTimeout(scrollTimer);
2774
+ scrollTimer = setTimeout(originalFunction, scrollDelayMs);
2775
+ });
2776
+
2777
+ polyfillEvent = EV_SCROLL;
2778
+ }
2779
+
2780
+ return {
2781
+ listener: polyfillFunction,
2782
+ eventType: polyfillEvent
2783
+ };
2784
+ }
2785
+
2786
+ function hasInlineEventHandlers(element) {
2787
+ for (var i = 0; i < EVENT_HANDLER_ATTRIBUTES.length; i++) {
2788
+ if (element.hasAttribute(EVENT_HANDLER_ATTRIBUTES[i])) {
2789
+ return true;
2790
+ }
2791
+ }
2792
+ return false;
2793
+ }
2794
+
2795
+ function hasInteractiveAriaRole(element) {
2796
+ var role = element.getAttribute('role');
2797
+ if (!role) return false;
2798
+
2799
+ // Handle invalid markup where multiple roles might be specified
2800
+ // Only the first token is recognized per ARIA spec
2801
+ var primaryRole = role.trim().split(/\s+/)[0].toLowerCase();
2802
+
2803
+ return INTERACTIVE_ARIA_ROLES[primaryRole];
2804
+ }
2805
+
2806
+ function hasAnyInteractivityIndicators(element) {
2807
+ var tagName = element.tagName.toLowerCase();
2808
+
2809
+ // Check for interactive HTML elements
2810
+ if (tagName === 'button' ||
2811
+ tagName === 'input' ||
2812
+ tagName === 'select' ||
2813
+ tagName === 'textarea' ||
2814
+ tagName === 'details' ||
2815
+ tagName === 'dialog') {
2816
+ return true;
2817
+ }
2818
+
2819
+ if (element.isContentEditable) {
2820
+ return true;
2821
+ }
2822
+
2823
+ if (element.onclick || element.onmousedown || element.onmouseup || element.ontouchstart || element.ontouchend) {
2824
+ return true;
2825
+ }
2826
+
2827
+ if (hasInlineEventHandlers(element)) {
2828
+ return true;
2829
+ }
2830
+
2831
+ if (hasInteractiveAriaRole(element)) {
2832
+ return true;
2833
+ }
2834
+
2835
+ if (tagName === 'a' && element.hasAttribute('href')) {
2836
+ return true;
2837
+ }
2838
+
2839
+ if (element.hasAttribute('tabindex')) {
2840
+ return true;
2841
+ }
2842
+
2843
+ return false;
2844
+ }
2845
+
2846
+
2847
+ function isDefinitelyNonInteractive(element) {
2848
+ if (!element || !element.tagName) {
2849
+ return true;
2850
+ }
2851
+
2852
+ var tagName = element.tagName.toLowerCase();
2853
+
2854
+ // These tags are definitely non-interactive
2855
+ if (ALWAYS_NON_INTERACTIVE_TAGS[tagName]) {
2856
+ return true;
2857
+ }
2858
+
2859
+ // For all other elements, we can only be certain they're non-interactive if they lack ALL indicators of interactivity
2860
+ // Check for any signs of interactivity
2861
+ if (hasAnyInteractivityIndicators(element)) {
2862
+ return false;
2863
+ }
2864
+
2865
+ // Check parent chain for interactive context
2866
+ var parent = element.parentElement;
2867
+ var depth = 0;
2868
+
2869
+ while (parent && depth < MAX_DEPTH) {
2870
+ if (hasAnyInteractivityIndicators(parent)) {
2871
+ return false; // Element is inside an interactive parent
2872
+ }
2873
+
2874
+ if (parent.getRootNode && parent.getRootNode() !== document$1) {
2875
+ var root = parent.getRootNode();
2876
+ if (root.host && hasAnyInteractivityIndicators(root.host)) {
2877
+ return false; // Inside an interactive shadow host
2878
+ }
2879
+ }
2880
+
2881
+ parent = parent.parentElement;
2882
+ depth++;
2883
+ }
2884
+
2885
+ // Pure text containers without any interactive context
2886
+ if (TEXT_CONTAINER_TAGS[tagName]) {
2887
+ // These are non-interactive ONLY if they have no interactive indicators (already checked as part of hasAnyInteractivityIndicators)
2888
+ return true;
2889
+ }
2890
+
2891
+ // Default: we can't be certain it's non-interactive
2892
+ return false;
2893
+ }
2894
+
2657
2895
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
2658
2896
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
2659
2897
  /** @const */ var DEFAULT_RAGE_CLICK_CLICK_COUNT = 4;
@@ -2686,6 +2924,350 @@ RageClickTracker.prototype.isRageClick = function(x, y, options) {
2686
2924
  return false;
2687
2925
  };
2688
2926
 
2927
+ function ShadowDOMObserver(changeCallback, observerConfig) {
2928
+ this.changeCallback = changeCallback || function() {};
2929
+ this.observerConfig = observerConfig;
2930
+
2931
+ this.observedShadowRoots = null;
2932
+ this.shadowObservers = [];
2933
+ }
2934
+
2935
+ ShadowDOMObserver.prototype.getEventTarget = function(event) {
2936
+ if (!this.observedShadowRoots) {
2937
+ return;
2938
+ }
2939
+ var path = this.getComposedPath(event);
2940
+ if (path && path.length) {
2941
+ return path[0];
2942
+ }
2943
+
2944
+ return event['target'] || event['srcElement'];
2945
+ };
2946
+
2947
+
2948
+ ShadowDOMObserver.prototype.getComposedPath = function(event) {
2949
+ if ('composedPath' in event) {
2950
+ return event['composedPath']();
2951
+ }
2952
+
2953
+ return [];
2954
+ };
2955
+ ShadowDOMObserver.prototype.observeFromEvent = function(event) {
2956
+ if (!this.observedShadowRoots) {
2957
+ return;
2958
+ }
2959
+
2960
+ var path = this.getComposedPath(event);
2961
+
2962
+ // Check each element in path for shadow roots
2963
+ for (var i = 0; i < path.length; i++) {
2964
+ var element = path[i];
2965
+
2966
+ if (element && element.shadowRoot) {
2967
+ this.observeShadowRoot(element.shadowRoot);
2968
+ }
2969
+ }
2970
+ };
2971
+
2972
+
2973
+ ShadowDOMObserver.prototype.observeShadowRoot = function(shadowRoot) {
2974
+ if (!this.observedShadowRoots || this.observedShadowRoots.has(shadowRoot)) {
2975
+ return;
2976
+ }
2977
+
2978
+ var self = this;
2979
+
2980
+ try {
2981
+ this.observedShadowRoots.add(shadowRoot);
2982
+
2983
+ var observer = new window.MutationObserver(function() {
2984
+ self.changeCallback();
2985
+ });
2986
+
2987
+ observer.observe(shadowRoot, this.observerConfig);
2988
+ this.shadowObservers.push(observer);
2989
+ } catch (e) {
2990
+ logger$4.critical('Error while observing shadow root', e);
2991
+ }
2992
+ };
2993
+
2994
+
2995
+ ShadowDOMObserver.prototype.start = function() {
2996
+ if (this.observedShadowRoots) {
2997
+ return;
2998
+ }
2999
+
3000
+ if (!weakSetSupported()) {
3001
+ logger$4.critical('Shadow DOM observation unavailable: WeakSet not supported');
3002
+ return;
3003
+ }
3004
+
3005
+ this.observedShadowRoots = new WeakSet();
3006
+ };
3007
+
3008
+ ShadowDOMObserver.prototype.stop = function() {
3009
+ if (!this.observedShadowRoots) {
3010
+ return;
3011
+ }
3012
+
3013
+ for (var i = 0; i < this.shadowObservers.length; i++) {
3014
+ try {
3015
+ this.shadowObservers[i].disconnect();
3016
+ } catch (e) {
3017
+ logger$4.critical('Error while disconnecting shadow DOM observer', e);
3018
+ }
3019
+ }
3020
+ this.shadowObservers = [];
3021
+ this.observedShadowRoots = null;
3022
+ };
3023
+
3024
+ /** @const */ var DEFAULT_DEAD_CLICK_TIMEOUT_MS = 500;
3025
+ /** @const */ var INTERACTION_EVENTS = [EV_CHANGE, EV_INPUT, EV_SUBMIT, EV_SELECT, EV_TOGGLE];
3026
+ /** @const */ var LAYOUT_EVENTS = [EV_SCROLLEND];
3027
+ /** @const */ var NAVIGATION_EVENTS = [EV_HASHCHANGE];
3028
+ /** @const */ var MUTATION_OBSERVER_CONFIG = {
3029
+ characterData: true,
3030
+ childList: true,
3031
+ subtree: true,
3032
+ attributes: true,
3033
+ attributeFilter: ['style', 'class', 'hidden', 'checked', 'selected', 'value', 'display', 'visibility']
3034
+ };
3035
+
3036
+
3037
+ function DeadClickTracker(onDeadClickCallback) {
3038
+ this.eventListeners = [];
3039
+ this.mutationObserver = null;
3040
+ this.shadowDOMObserver = null;
3041
+
3042
+ this.isTracking = false;
3043
+ this.lastChangeEventTimestamp = 0;
3044
+ this.pendingClicks = [];
3045
+ this.onDeadClickCallback = onDeadClickCallback;
3046
+ this.processingActive = false;
3047
+ this.processingTimeout = null;
3048
+ }
3049
+
3050
+
3051
+ DeadClickTracker.prototype.addClick = function(event) {
3052
+ var element = this.shadowDOMObserver && this.shadowDOMObserver.getEventTarget(event);
3053
+
3054
+ if (!element) {
3055
+ element = event['target'] || event['srcElement'];
3056
+ }
3057
+
3058
+ if (!element || isDefinitelyNonInteractive(element)) {
3059
+ return false;
3060
+ }
3061
+
3062
+ if (this.shadowDOMObserver) {
3063
+ this.shadowDOMObserver.observeFromEvent(event);
3064
+ }
3065
+ this.pendingClicks.push({
3066
+ element: element,
3067
+ event: event,
3068
+ timestamp: Date.now()
3069
+ });
3070
+ return true;
3071
+ };
3072
+
3073
+ DeadClickTracker.prototype.trackClick = function(event, config) {
3074
+ if (!this.isTracking) {
3075
+ return false;
3076
+ }
3077
+
3078
+ var added = this.addClick(event);
3079
+ if (added) {
3080
+ this.triggerProcessing(config);
3081
+ }
3082
+ return added;
3083
+ };
3084
+
3085
+ DeadClickTracker.prototype.getDeadClicks = function(config) {
3086
+ if (this.pendingClicks.length === 0) {
3087
+ return [];
3088
+ }
3089
+
3090
+ var timeoutMs = config['timeout_ms'];
3091
+ var now = Date.now();
3092
+ var clicksToEvaluate = this.pendingClicks.slice(); // Copy array
3093
+ this.pendingClicks = []; // Clear original
3094
+
3095
+ var deadClicks = [];
3096
+
3097
+ for (var i = 0; i < clicksToEvaluate.length; i++) {
3098
+ var click = clicksToEvaluate[i];
3099
+
3100
+ if (now - click.timestamp >= timeoutMs) {
3101
+ // Click has exceeded timeout, check if it's dead by looking for changes after this specific click
3102
+ if (!this.hasChangesAfter(click.timestamp)) {
3103
+ deadClicks.push(click);
3104
+ }
3105
+ } else {
3106
+ // Still pending - add back
3107
+ this.pendingClicks.push(click);
3108
+ }
3109
+ }
3110
+
3111
+ return deadClicks;
3112
+ };
3113
+
3114
+ DeadClickTracker.prototype.hasChangesAfter = function(timestamp) {
3115
+ // 100ms tolerance for race condition between when we record the click and the change event
3116
+ return this.lastChangeEventTimestamp >= (timestamp - 100);
3117
+ };
3118
+
3119
+ DeadClickTracker.prototype.recordChangeEvent = function() {
3120
+ this.lastChangeEventTimestamp = Date.now();
3121
+ };
3122
+
3123
+ DeadClickTracker.prototype.triggerProcessing = function(config) {
3124
+ // Prevent multiple concurrent processing chains
3125
+ if (this.processingActive) {
3126
+ return;
3127
+ }
3128
+ this.processingActive = true;
3129
+ this.processRecursively(config);
3130
+ };
3131
+
3132
+ DeadClickTracker.prototype.processRecursively = function(config) {
3133
+ if (!this.isTracking || !this.onDeadClickCallback) {
3134
+ this.processingActive = false;
3135
+ return;
3136
+ }
3137
+
3138
+ var timeoutMs = config['timeout_ms'];
3139
+ var self = this;
3140
+
3141
+ this.processingTimeout = setTimeout(function() {
3142
+ if (!self.processingActive) {
3143
+ return;
3144
+ }
3145
+
3146
+ var deadClicks = self.getDeadClicks(config);
3147
+
3148
+ for (var i = 0; i < deadClicks.length; i++) {
3149
+ self.onDeadClickCallback(deadClicks[i].event);
3150
+ }
3151
+
3152
+ if (self.pendingClicks.length > 0) {
3153
+ self.processRecursively(config);
3154
+ } else {
3155
+ self.processingActive = false;
3156
+ }
3157
+ }, timeoutMs);
3158
+ };
3159
+
3160
+ DeadClickTracker.prototype.startTracking = function() {
3161
+ if (this.isTracking) {
3162
+ return;
3163
+ }
3164
+
3165
+ this.isTracking = true;
3166
+
3167
+ var self = this;
3168
+
3169
+ INTERACTION_EVENTS.forEach(function(event) {
3170
+ var handler = function() {
3171
+ self.recordChangeEvent();
3172
+ };
3173
+ document.addEventListener(event, handler, { capture: true, passive: true });
3174
+ self.eventListeners.push({ target: document, event: event, handler: handler, options: { capture: true, passive: true } });
3175
+ });
3176
+ NAVIGATION_EVENTS.forEach(function(event) {
3177
+ var handler = function() {
3178
+ self.recordChangeEvent();
3179
+ };
3180
+ window.addEventListener(event, handler);
3181
+ self.eventListeners.push({ target: window, event: event, handler: handler });
3182
+ });
3183
+ LAYOUT_EVENTS.forEach(function(event) {
3184
+ var handler = function() {
3185
+ self.recordChangeEvent();
3186
+ };
3187
+ window.addEventListener(event, handler, { passive: true });
3188
+ self.eventListeners.push({ target: window, event: event, handler: handler, options: { passive: true } });
3189
+ });
3190
+ var selectionHandler = function() {
3191
+ self.recordChangeEvent();
3192
+ };
3193
+ document.addEventListener('selectionchange', selectionHandler);
3194
+ self.eventListeners.push({ target: document, event: 'selectionchange', handler: selectionHandler });
3195
+
3196
+ // Set up MutationObserver
3197
+ if (window.MutationObserver) {
3198
+ try {
3199
+ this.mutationObserver = new window.MutationObserver(function() {
3200
+ self.recordChangeEvent();
3201
+ });
3202
+
3203
+ this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
3204
+ } catch (e) {
3205
+ logger$4.critical('Error while setting up mutation observer', e);
3206
+ }
3207
+ }
3208
+
3209
+ // Set up Shadow DOM observer
3210
+ if (window.customElements) {
3211
+ try {
3212
+ this.shadowDOMObserver = new ShadowDOMObserver(
3213
+ function() {
3214
+ self.recordChangeEvent();
3215
+ },
3216
+ MUTATION_OBSERVER_CONFIG
3217
+ );
3218
+ this.shadowDOMObserver.start();
3219
+ } catch (e) {
3220
+ logger$4.critical('Error while setting up shadow DOM observer', e);
3221
+ this.shadowDOMObserver = null;
3222
+ }
3223
+ }
3224
+ };
3225
+
3226
+ DeadClickTracker.prototype.stopTracking = function() {
3227
+ if (!this.isTracking) {
3228
+ return;
3229
+ }
3230
+
3231
+ this.isTracking = false;
3232
+ this.pendingClicks = [];
3233
+ this.lastChangeEventTimestamp = 0;
3234
+ this.processingActive = false;
3235
+
3236
+ if (this.processingTimeout) {
3237
+ clearTimeout(this.processingTimeout);
3238
+ this.processingTimeout = null;
3239
+ }
3240
+
3241
+ // Remove all event listeners
3242
+ for (var i = 0; i < this.eventListeners.length; i++) {
3243
+ var listener = this.eventListeners[i];
3244
+ try {
3245
+ listener.target.removeEventListener(listener.event, listener.handler, listener.options);
3246
+ } catch (e) {
3247
+ logger$4.critical('Error while removing event listener', e);
3248
+ }
3249
+ }
3250
+ this.eventListeners = [];
3251
+
3252
+ if (this.mutationObserver) {
3253
+ try {
3254
+ this.mutationObserver.disconnect();
3255
+ } catch (e) {
3256
+ logger$4.critical('Error while disconnecting mutation observer', e);
3257
+ }
3258
+ this.mutationObserver = null;
3259
+ }
3260
+
3261
+ if (this.shadowDOMObserver) {
3262
+ try {
3263
+ this.shadowDOMObserver.stop();
3264
+ } catch (e) {
3265
+ logger$4.critical('Error while stopping shadow DOM observer', e);
3266
+ }
3267
+ this.shadowDOMObserver = null;
3268
+ }
3269
+ };
3270
+
2689
3271
  var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
2690
3272
  var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
2691
3273
 
@@ -2705,10 +3287,12 @@ var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
2705
3287
  var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
2706
3288
  var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
2707
3289
  var CONFIG_TRACK_CLICK = 'click';
3290
+ var CONFIG_TRACK_DEAD_CLICK = 'dead_click';
2708
3291
  var CONFIG_TRACK_INPUT = 'input';
2709
3292
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
2710
3293
  var CONFIG_TRACK_RAGE_CLICK = 'rage_click';
2711
3294
  var CONFIG_TRACK_SCROLL = 'scroll';
3295
+ var CONFIG_TRACK_PAGE_LEAVE = 'page_leave';
2712
3296
  var CONFIG_TRACK_SUBMIT = 'submit';
2713
3297
 
2714
3298
  var CONFIG_DEFAULTS$1 = {};
@@ -2723,10 +3307,12 @@ CONFIG_DEFAULTS$1[CONFIG_CAPTURE_TEXT_CONTENT] = false;
2723
3307
  CONFIG_DEFAULTS$1[CONFIG_SCROLL_CAPTURE_ALL] = false;
2724
3308
  CONFIG_DEFAULTS$1[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
2725
3309
  CONFIG_DEFAULTS$1[CONFIG_TRACK_CLICK] = true;
3310
+ CONFIG_DEFAULTS$1[CONFIG_TRACK_DEAD_CLICK] = true;
2726
3311
  CONFIG_DEFAULTS$1[CONFIG_TRACK_INPUT] = true;
2727
3312
  CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
2728
3313
  CONFIG_DEFAULTS$1[CONFIG_TRACK_RAGE_CLICK] = true;
2729
3314
  CONFIG_DEFAULTS$1[CONFIG_TRACK_SCROLL] = true;
3315
+ CONFIG_DEFAULTS$1[CONFIG_TRACK_PAGE_LEAVE] = false;
2730
3316
  CONFIG_DEFAULTS$1[CONFIG_TRACK_SUBMIT] = true;
2731
3317
 
2732
3318
  var DEFAULT_PROPS = {
@@ -2734,10 +3320,12 @@ var DEFAULT_PROPS = {
2734
3320
  };
2735
3321
 
2736
3322
  var MP_EV_CLICK = '$mp_click';
3323
+ var MP_EV_DEAD_CLICK = '$mp_dead_click';
2737
3324
  var MP_EV_INPUT = '$mp_input_change';
2738
3325
  var MP_EV_RAGE_CLICK = '$mp_rage_click';
2739
3326
  var MP_EV_SCROLL = '$mp_scroll';
2740
3327
  var MP_EV_SUBMIT = '$mp_submit';
3328
+ var MP_EV_PAGE_LEAVE = '$mp_page_leave';
2741
3329
 
2742
3330
  /**
2743
3331
  * Autocapture: manages automatic event tracking
@@ -2745,6 +3333,9 @@ var MP_EV_SUBMIT = '$mp_submit';
2745
3333
  */
2746
3334
  var Autocapture = function(mp) {
2747
3335
  this.mp = mp;
3336
+ this.maxScrollViewDepth = 0;
3337
+ this.hasTrackedScrollSession = false;
3338
+ this.previousScrollHeight = 0;
2748
3339
  };
2749
3340
 
2750
3341
  Autocapture.prototype.init = function() {
@@ -2752,13 +3343,15 @@ Autocapture.prototype.init = function() {
2752
3343
  logger$4.critical('Autocapture unavailable: missing required DOM APIs');
2753
3344
  return;
2754
3345
  }
2755
-
3346
+ this.initPageListeners();
2756
3347
  this.initPageviewTracking();
2757
3348
  this.initClickTracking();
3349
+ this.initDeadClickTracking();
2758
3350
  this.initInputTracking();
2759
3351
  this.initScrollTracking();
2760
3352
  this.initSubmitTracking();
2761
3353
  this.initRageClickTracking();
3354
+ this.initPageLeaveTracking();
2762
3355
  };
2763
3356
 
2764
3357
  Autocapture.prototype.getFullConfig = function() {
@@ -2839,7 +3432,8 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
2839
3432
 
2840
3433
  var isCapturedForHeatMap = this.mp.is_recording_heatmap_data() && (
2841
3434
  (mpEventName === MP_EV_CLICK && !this.getConfig(CONFIG_TRACK_CLICK)) ||
2842
- (mpEventName === MP_EV_RAGE_CLICK && !this._getRageClickConfig())
3435
+ (mpEventName === MP_EV_RAGE_CLICK && !this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK)) ||
3436
+ (mpEventName === MP_EV_DEAD_CLICK && !this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK))
2843
3437
  );
2844
3438
 
2845
3439
  var props = getPropsForDOMEvent(ev, {
@@ -2858,11 +3452,45 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
2858
3452
  }
2859
3453
  };
2860
3454
 
2861
- Autocapture.prototype._getRageClickConfig = function() {
2862
- var config = this.getConfig(CONFIG_TRACK_RAGE_CLICK);
3455
+ Autocapture.prototype.initPageListeners = function() {
3456
+ win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
3457
+ win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
3458
+
3459
+ if (!this.pageviewTrackingConfig() && !this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.get_config('record_heatmap_data')) {
3460
+ // These are all the configs that use these listeners
3461
+ return;
3462
+ }
3463
+
3464
+ this.listenerPopstate = function() {
3465
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3466
+ };
3467
+ this.listenerHashchange = function() {
3468
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3469
+ };
3470
+
3471
+ win.addEventListener(EV_POPSTATE, this.listenerPopstate);
3472
+ win.addEventListener(EV_HASHCHANGE, this.listenerHashchange);
3473
+ var nativePushState = win.history.pushState;
3474
+ if (typeof nativePushState === 'function') {
3475
+ win.history.pushState = function(state, unused, url) {
3476
+ nativePushState.call(win.history, state, unused, url);
3477
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3478
+ };
3479
+ }
3480
+ var nativeReplaceState = win.history.replaceState;
3481
+ if (typeof nativeReplaceState === 'function') {
3482
+ win.history.replaceState = function(state, unused, url) {
3483
+ nativeReplaceState.call(win.history, state, unused, url);
3484
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
3485
+ };
3486
+ }
3487
+ };
3488
+
3489
+ Autocapture.prototype._getClickTrackingConfig = function(configKey) {
3490
+ var config = this.getConfig(configKey);
2863
3491
 
2864
3492
  if (!config) {
2865
- return null; // rage click tracking disabled
3493
+ return null; // click tracking disabled
2866
3494
  }
2867
3495
 
2868
3496
  if (config === true) {
@@ -2876,6 +3504,70 @@ Autocapture.prototype._getRageClickConfig = function() {
2876
3504
  return {}; // fallback to defaults for any other truthy value
2877
3505
  };
2878
3506
 
3507
+ Autocapture.prototype._trackPageLeave = function(ev, currentUrl, currentScrollHeight) {
3508
+ if (this.hasTrackedScrollSession) {
3509
+ // User has navigated away already ending their impression.
3510
+ return;
3511
+ }
3512
+
3513
+ if (!this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.is_recording_heatmap_data()) {
3514
+ return;
3515
+ }
3516
+
3517
+ this.hasTrackedScrollSession = true;
3518
+ var viewportHeight = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3519
+ var scrollPercentage = Math.round(Math.max(this.maxScrollViewDepth - viewportHeight, 0) / (currentScrollHeight - viewportHeight) * 100);
3520
+ var foldLinePercentage = Math.round((viewportHeight / currentScrollHeight) * 100);
3521
+ if (currentScrollHeight <= viewportHeight) {
3522
+ // If the content fits within the viewport, consider it fully scrolled
3523
+ scrollPercentage = 100;
3524
+ foldLinePercentage = 100;
3525
+ }
3526
+
3527
+ var props = _.extend({
3528
+ '$max_scroll_view_depth': this.maxScrollViewDepth,
3529
+ '$max_scroll_percentage': scrollPercentage,
3530
+ '$fold_line_percentage': foldLinePercentage,
3531
+ '$scroll_height': currentScrollHeight,
3532
+ '$event_type': ev.type,
3533
+ '$current_url': currentUrl || _.info.currentUrl(),
3534
+ '$viewportHeight': viewportHeight, // This is the fold line
3535
+ '$viewportWidth': Math.max(document$1.documentElement.clientWidth, win.innerWidth || 0),
3536
+ '$captured_for_heatmap': this.mp.is_recording_heatmap_data()
3537
+ }, DEFAULT_PROPS);
3538
+
3539
+ // Send with beacon transport to ensure event is sent before unload
3540
+ this.mp.track(MP_EV_PAGE_LEAVE, props, {transport: 'sendBeacon'});
3541
+ };
3542
+
3543
+ Autocapture.prototype._initScrollDepthTracking = function() {
3544
+ win.removeEventListener(EV_SCROLL, this.listenerScrollDepth);
3545
+ win.removeEventListener(EV_SCROLLEND, this.listenerScrollDepth);
3546
+
3547
+ if (!this.mp.get_config('record_heatmap_data')) {
3548
+ return;
3549
+ }
3550
+
3551
+ logger$4.log('Initializing scroll depth tracking');
3552
+
3553
+ this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3554
+
3555
+ var updateScrollDepth = function() {
3556
+ if (this.currentUrlBlocked()) {
3557
+ return;
3558
+ }
3559
+ var scrollViewHeight = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0) + win.scrollY;
3560
+ if (scrollViewHeight > this.maxScrollViewDepth) {
3561
+ this.maxScrollViewDepth = scrollViewHeight;
3562
+ }
3563
+ this.previousScrollHeight = document$1.body.scrollHeight;
3564
+ }.bind(this);
3565
+
3566
+ var scrollEndPolyfill = getPolyfillScrollEndFunction(updateScrollDepth);
3567
+ this.listenerScrollDepth = scrollEndPolyfill.listener;
3568
+ win.addEventListener(scrollEndPolyfill.eventType, this.listenerScrollDepth);
3569
+ };
3570
+
2879
3571
  Autocapture.prototype.initClickTracking = function() {
2880
3572
  win.removeEventListener(EV_CLICK, this.listenerClick);
2881
3573
 
@@ -2884,12 +3576,49 @@ Autocapture.prototype.initClickTracking = function() {
2884
3576
  }
2885
3577
  logger$4.log('Initializing click tracking');
2886
3578
 
2887
- this.listenerClick = win.addEventListener(EV_CLICK, function(ev) {
3579
+ this.listenerClick = function(ev) {
2888
3580
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
2889
3581
  return;
2890
3582
  }
2891
3583
  this.trackDomEvent(ev, MP_EV_CLICK);
2892
- }.bind(this));
3584
+ }.bind(this);
3585
+ win.addEventListener(EV_CLICK, this.listenerClick);
3586
+ };
3587
+
3588
+ Autocapture.prototype.initDeadClickTracking = function() {
3589
+ var deadClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK);
3590
+
3591
+ if (!deadClickConfig && !this.mp.get_config('record_heatmap_data')) {
3592
+ this.stopDeadClickTracking();
3593
+ return;
3594
+ }
3595
+
3596
+ logger$4.log('Initializing dead click tracking');
3597
+ if (!this._deadClickTracker) {
3598
+ this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
3599
+ this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
3600
+ }.bind(this));
3601
+ this._deadClickTracker.startTracking();
3602
+ }
3603
+
3604
+ if (!this.listenerDeadClick) {
3605
+ this.listenerDeadClick = function(ev) {
3606
+ var currentDeadClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_DEAD_CLICK);
3607
+ if (!currentDeadClickConfig && !this.mp.is_recording_heatmap_data()) {
3608
+ return;
3609
+ }
3610
+ if (this.currentUrlBlocked()) {
3611
+ return;
3612
+ }
3613
+ // Normalize config to ensure timeout_ms is always set
3614
+ var normalizedConfig = currentDeadClickConfig || {};
3615
+ if (!normalizedConfig['timeout_ms']) {
3616
+ normalizedConfig['timeout_ms'] = DEFAULT_DEAD_CLICK_TIMEOUT_MS;
3617
+ }
3618
+ this._deadClickTracker.trackClick(ev, normalizedConfig);
3619
+ }.bind(this);
3620
+ win.addEventListener(EV_CLICK, this.listenerDeadClick);
3621
+ }
2893
3622
  };
2894
3623
 
2895
3624
  Autocapture.prototype.initInputTracking = function() {
@@ -2900,17 +3629,16 @@ Autocapture.prototype.initInputTracking = function() {
2900
3629
  }
2901
3630
  logger$4.log('Initializing input tracking');
2902
3631
 
2903
- this.listenerChange = win.addEventListener(EV_CHANGE, function(ev) {
3632
+ this.listenerChange = function(ev) {
2904
3633
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
2905
3634
  return;
2906
3635
  }
2907
3636
  this.trackDomEvent(ev, MP_EV_INPUT);
2908
- }.bind(this));
3637
+ }.bind(this);
3638
+ win.addEventListener(EV_CHANGE, this.listenerChange);
2909
3639
  };
2910
3640
 
2911
3641
  Autocapture.prototype.initPageviewTracking = function() {
2912
- win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
2913
- win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
2914
3642
  win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
2915
3643
 
2916
3644
  if (!this.pageviewTrackingConfig()) {
@@ -2927,27 +3655,7 @@ Autocapture.prototype.initPageviewTracking = function() {
2927
3655
  previousTrackedUrl = _.info.currentUrl();
2928
3656
  }
2929
3657
 
2930
- this.listenerPopstate = win.addEventListener(EV_POPSTATE, function() {
2931
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2932
- });
2933
- this.listenerHashchange = win.addEventListener(EV_HASHCHANGE, function() {
2934
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2935
- });
2936
- var nativePushState = win.history.pushState;
2937
- if (typeof nativePushState === 'function') {
2938
- win.history.pushState = function(state, unused, url) {
2939
- nativePushState.call(win.history, state, unused, url);
2940
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2941
- };
2942
- }
2943
- var nativeReplaceState = win.history.replaceState;
2944
- if (typeof nativeReplaceState === 'function') {
2945
- win.history.replaceState = function(state, unused, url) {
2946
- nativeReplaceState.call(win.history, state, unused, url);
2947
- win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2948
- };
2949
- }
2950
- this.listenerLocationchange = win.addEventListener(EV_MP_LOCATION_CHANGE, safewrap(function() {
3658
+ this.listenerLocationchange = safewrap(function() {
2951
3659
  if (this.currentUrlBlocked()) {
2952
3660
  return;
2953
3661
  }
@@ -2974,13 +3682,14 @@ Autocapture.prototype.initPageviewTracking = function() {
2974
3682
  logger$4.log('Path change: re-initializing scroll depth checkpoints');
2975
3683
  }
2976
3684
  }
2977
- }.bind(this)));
3685
+ }.bind(this));
3686
+ win.addEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
2978
3687
  };
2979
3688
 
2980
3689
  Autocapture.prototype.initRageClickTracking = function() {
2981
3690
  win.removeEventListener(EV_CLICK, this.listenerRageClick);
2982
3691
 
2983
- var rageClickConfig = this._getRageClickConfig();
3692
+ var rageClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK);
2984
3693
  if (!rageClickConfig && !this.mp.get_config('record_heatmap_data')) {
2985
3694
  return;
2986
3695
  }
@@ -2991,7 +3700,7 @@ Autocapture.prototype.initRageClickTracking = function() {
2991
3700
  }
2992
3701
 
2993
3702
  this.listenerRageClick = function(ev) {
2994
- var currentRageClickConfig = this._getRageClickConfig();
3703
+ var currentRageClickConfig = this._getClickTrackingConfig(CONFIG_TRACK_RAGE_CLICK);
2995
3704
  if (!currentRageClickConfig && !this.mp.is_recording_heatmap_data()) {
2996
3705
  return;
2997
3706
  }
@@ -3009,6 +3718,8 @@ Autocapture.prototype.initRageClickTracking = function() {
3009
3718
 
3010
3719
  Autocapture.prototype.initScrollTracking = function() {
3011
3720
  win.removeEventListener(EV_SCROLLEND, this.listenerScroll);
3721
+ win.removeEventListener(EV_SCROLL, this.listenerScroll);
3722
+
3012
3723
 
3013
3724
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3014
3725
  return;
@@ -3016,7 +3727,7 @@ Autocapture.prototype.initScrollTracking = function() {
3016
3727
  logger$4.log('Initializing scroll tracking');
3017
3728
  this.lastScrollCheckpoint = 0;
3018
3729
 
3019
- this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
3730
+ var scrollTrackFunction = function() {
3020
3731
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3021
3732
  return;
3022
3733
  }
@@ -3055,7 +3766,11 @@ Autocapture.prototype.initScrollTracking = function() {
3055
3766
  if (shouldTrack) {
3056
3767
  this.mp.track(MP_EV_SCROLL, props);
3057
3768
  }
3058
- }.bind(this)));
3769
+ }.bind(this);
3770
+
3771
+ var scrollEndPolyfill = getPolyfillScrollEndFunction(scrollTrackFunction);
3772
+ this.listenerScroll = scrollEndPolyfill.listener;
3773
+ win.addEventListener(scrollEndPolyfill.eventType, this.listenerScroll);
3059
3774
  };
3060
3775
 
3061
3776
  Autocapture.prototype.initSubmitTracking = function() {
@@ -3066,18 +3781,81 @@ Autocapture.prototype.initSubmitTracking = function() {
3066
3781
  }
3067
3782
  logger$4.log('Initializing submit tracking');
3068
3783
 
3069
- this.listenerSubmit = win.addEventListener(EV_SUBMIT, function(ev) {
3784
+ this.listenerSubmit = function(ev) {
3070
3785
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
3071
3786
  return;
3072
3787
  }
3073
3788
  this.trackDomEvent(ev, MP_EV_SUBMIT);
3789
+ }.bind(this);
3790
+ win.addEventListener(EV_SUBMIT, this.listenerSubmit);
3791
+ };
3792
+
3793
+ Autocapture.prototype.initPageLeaveTracking = function() {
3794
+ // Capture page_leave both when the user navigates away from the page (visibilitychange) as well
3795
+ // as when they navigate to a different page within the SPA (popstate/pushstate/hashchange).
3796
+ document$1.removeEventListener(EV_VISIBILITYCHANGE, this.listenerPageLeaveVisibilitychange);
3797
+ win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerPageLeaveLocationchange);
3798
+ win.removeEventListener(EV_LOAD, this.listenerPageLoad);
3799
+
3800
+ if (!this.getConfig(CONFIG_TRACK_PAGE_LEAVE) && !this.mp.get_config('record_heatmap_data')) {
3801
+ return;
3802
+ }
3803
+
3804
+ logger$4.log('Initializing page visibility tracking.');
3805
+ this._initScrollDepthTracking();
3806
+ var previousTrackedUrl = _.info.currentUrl();
3807
+
3808
+ // Initialize previousScrollHeight on `load` which handles async loading
3809
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
3810
+ this.listenerPageLoad = function() {
3811
+ this.previousScrollHeight = document$1.body.scrollHeight;
3812
+ }.bind(this);
3813
+ win.addEventListener(EV_LOAD, this.listenerPageLoad);
3814
+
3815
+ // Track page navigation events similar to how initPageviewTracking does it
3816
+ this.listenerPageLeaveLocationchange = safewrap(function(ev) {
3817
+ if (this.currentUrlBlocked()) {
3818
+ return;
3819
+ }
3820
+
3821
+ var currentUrl = _.info.currentUrl();
3822
+ // Track all URL changes including query string or fragment changes as separate scroll sessions
3823
+ var shouldTrack = currentUrl !== previousTrackedUrl;
3824
+
3825
+ if (shouldTrack) {
3826
+ this._trackPageLeave(ev, previousTrackedUrl, this.previousScrollHeight);
3827
+ previousTrackedUrl = currentUrl;
3828
+ // Fragment navigation should call scroll(end) and trigger listener, don't add window.scrollY here.
3829
+ this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3830
+ this.previousScrollHeight = document$1.body.scrollHeight;
3831
+ this.hasTrackedScrollSession = false;
3832
+ }
3074
3833
  }.bind(this));
3834
+ win.addEventListener(EV_MP_LOCATION_CHANGE, this.listenerPageLeaveLocationchange);
3835
+
3836
+ this.listenerPageLeaveVisibilitychange = function(ev) {
3837
+ if (document$1.hidden) {
3838
+ this._trackPageLeave(ev, previousTrackedUrl, this.previousScrollHeight);
3839
+ }
3840
+ }.bind(this);
3841
+ document$1.addEventListener(EV_VISIBILITYCHANGE, this.listenerPageLeaveVisibilitychange);
3842
+ };
3843
+
3844
+ Autocapture.prototype.stopDeadClickTracking = function() {
3845
+ if (this.listenerDeadClick) {
3846
+ win.removeEventListener(EV_CLICK, this.listenerDeadClick);
3847
+ this.listenerDeadClick = null;
3848
+ }
3849
+
3850
+ if (this._deadClickTracker) {
3851
+ this._deadClickTracker.stopTracking();
3852
+ this._deadClickTracker = null;
3853
+ }
3075
3854
  };
3076
3855
 
3077
3856
  // TODO integrate error_reporter from mixpanel instance
3078
3857
  safewrapClass(Autocapture);
3079
3858
 
3080
- var fetch = win['fetch'];
3081
3859
  var logger$3 = console_with_prefix('flags');
3082
3860
 
3083
3861
  var FLAGS_CONFIG_KEY = 'flags';
@@ -3091,6 +3869,7 @@ CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
3091
3869
  * @constructor
3092
3870
  */
3093
3871
  var FeatureFlagManager = function(initOptions) {
3872
+ this.fetch = win['fetch'];
3094
3873
  this.getFullApiRoute = initOptions.getFullApiRoute;
3095
3874
  this.getMpConfig = initOptions.getConfigFunc;
3096
3875
  this.setMpConfig = initOptions.setConfigFunc;
@@ -3099,7 +3878,7 @@ var FeatureFlagManager = function(initOptions) {
3099
3878
  };
3100
3879
 
3101
3880
  FeatureFlagManager.prototype.init = function() {
3102
- if (!minApisSupported()) {
3881
+ if (!this.minApisSupported()) {
3103
3882
  logger$3.critical('Feature Flags unavailable: missing minimum required APIs');
3104
3883
  return;
3105
3884
  }
@@ -3162,6 +3941,7 @@ FeatureFlagManager.prototype.fetchFlags = function() {
3162
3941
 
3163
3942
  var distinctId = this.getMpProperty('distinct_id');
3164
3943
  var deviceId = this.getMpProperty('$device_id');
3944
+ var traceparent = generateTraceparent();
3165
3945
  logger$3.log('Fetching flags for distinct ID: ' + distinctId);
3166
3946
 
3167
3947
  var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
@@ -3173,10 +3953,11 @@ FeatureFlagManager.prototype.fetchFlags = function() {
3173
3953
  var url = this.getFullApiRoute() + '?' + searchParams.toString();
3174
3954
 
3175
3955
  this._fetchInProgressStartTime = Date.now();
3176
- this.fetchPromise = win['fetch'](url, {
3956
+ this.fetchPromise = this.fetch.call(win, url, {
3177
3957
  'method': 'GET',
3178
3958
  'headers': {
3179
- 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':')
3959
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
3960
+ 'traceparent': traceparent
3180
3961
  }
3181
3962
  }).then(function(response) {
3182
3963
  this.markFetchComplete();
@@ -3189,10 +3970,14 @@ FeatureFlagManager.prototype.fetchFlags = function() {
3189
3970
  _.each(responseFlags, function(data, key) {
3190
3971
  flags.set(key, {
3191
3972
  'key': data['variant_key'],
3192
- 'value': data['variant_value']
3973
+ 'value': data['variant_value'],
3974
+ 'experiment_id': data['experiment_id'],
3975
+ 'is_experiment_active': data['is_experiment_active'],
3976
+ 'is_qa_tester': data['is_qa_tester']
3193
3977
  });
3194
3978
  });
3195
3979
  this.flags = flags;
3980
+ this._traceparent = traceparent;
3196
3981
  }.bind(this)).catch(function(error) {
3197
3982
  this.markFetchComplete();
3198
3983
  logger$3.error(error);
@@ -3289,22 +4074,36 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
3289
4074
  return;
3290
4075
  }
3291
4076
  this.trackedFeatures.add(featureName);
3292
- this.track('$experiment_started', {
4077
+
4078
+ var trackingProperties = {
3293
4079
  'Experiment name': featureName,
3294
4080
  'Variant name': feature['key'],
3295
4081
  '$experiment_type': 'feature_flag',
3296
4082
  'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
3297
4083
  'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
3298
- 'Variant fetch latency (ms)': this._fetchLatency
3299
- });
4084
+ 'Variant fetch latency (ms)': this._fetchLatency,
4085
+ 'Variant fetch traceparent': this._traceparent,
4086
+ };
4087
+
4088
+ if (feature['experiment_id'] !== 'undefined') {
4089
+ trackingProperties['$experiment_id'] = feature['experiment_id'];
4090
+ }
4091
+ if (feature['is_experiment_active'] !== 'undefined') {
4092
+ trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
4093
+ }
4094
+ if (feature['is_qa_tester'] !== 'undefined') {
4095
+ trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
4096
+ }
4097
+
4098
+ this.track('$experiment_started', trackingProperties);
3300
4099
  };
3301
4100
 
3302
- function minApisSupported() {
3303
- return !!fetch &&
4101
+ FeatureFlagManager.prototype.minApisSupported = function() {
4102
+ return !!this.fetch &&
3304
4103
  typeof Promise !== 'undefined' &&
3305
4104
  typeof Map !== 'undefined' &&
3306
4105
  typeof Set !== 'undefined';
3307
- }
4106
+ };
3308
4107
 
3309
4108
  safewrapClass(FeatureFlagManager);
3310
4109