1/* 2 * INP collection for the server-rendered pages. 3 * 4 * These pages never boot the Angular SPA, so WebVitalsService â which only runs 5 * from AppComponent â has never seen them and they emit no WebVital events at 6 * all. Rather than pull the web-vitals bundle and the Application Insights SDK 7 * into a page whose whole point is a fast first paint, this posts a single 8 * pre-built envelope to the ingestion endpoint by beacon when the page hides. 9 * 10 * The candidate-selection below mirrors web-vitals' own estimator so the 11 * numbers stay comparable with the SPA's: the worst interaction for the first 12 * 50, then the second-worst, and so on. Diverging here would make the two page 13 * families look different when only the measurement differed. 14 * 15 * Configuration arrives as window.AW_RUM from the _WebVitalsRum partial. 16 */ 17(function () { 18 'use strict'; 19 20 var config = window.AW_RUM; 21 22 if (!config || !config.instrumentationKey || typeof PerformanceObserver !== 'function' || !navigator.sendBeacon) { 23 return; 24 } 25 26 // web-vitals reports one candidate per 50 interactions and keeps 10 in hand. 27 var MAX_CANDIDATES = 10; 28 var INTERACTIONS_PER_CANDIDATE = 50; 29 // Matches web-vitals' default durationThreshold: below this an interaction 30 // cannot affect a p75 that is anywhere near the 200ms "good" boundary. 31 var DURATION_THRESHOLD = 40; 32 33 var interactionsByIdentifier = {}; 34 var candidates = []; 35 var interactionCount = 0; 36 var reported = false; 37 var longAnimationFrames = []; 38 39 function describeTarget(node) { 40 if (!node || node.nodeType !== 1) { 41 return ''; 42 } 43 44 var parts = []; 45 var current = node; 46 var depth = 0; 47 48 while (current && current.nodeType === 1 && depth < 3) { 49 var part = current.nodeName.toLowerCase(); 50 51 if (current.id) { 52 parts.unshift(part + '#' + current.id); 53 break; 54 } 55 56 var className = typeof current.className === 'string' ? current.className.trim().split(/\s+/)[0] : ''; 57 if (className) { 58 part += '.' + className; 59 } 60 61 parts.unshift(part); 62 current = current.parentElement; 63 depth++; 64 } 65 66 return parts.join('>'); 67 } 68 69 function recordEntry(entry) { 70 if (!entry.interactionId) { 71 return; 72 } 73 74 var existing = interactionsByIdentifier[entry.interactionId]; 75 76 if (existing) { 77 // Several event entries (pointerdown, pointerup, click) share one 78 // interactionId; the interaction's duration is the longest of them. 79 if (entry.duration > existing.duration) { 80 interactionsByIdentifier[entry.interactionId] = toInteraction(entry); 81 } 82 return; 83 } 84 85 interactionCount++; 86 interactionsByIdentifier[entry.interactionId] = toInteraction(entry); 87 } 88 89 function toInteraction(entry) { 90 var processingStart = entry.processingStart != null ? entry.processingStart : entry.startTime; 91 var processingEnd = entry.processingEnd != null ? entry.processingEnd : processingStart; 92 93 return { 94 duration: entry.duration, 95 startTime: entry.startTime, 96 inputDelay: processingStart - entry.startTime, 97 processingDuration: processingEnd - processingStart, 98 presentationDelay: Math.max(0, entry.startTime + entry.duration - processingEnd), 99 interactionType: entry.name.indexOf('key') === 0 ? 'keyboard' : 'pointer', 100 interactionTarget: describeTarget(entry.target) 101 }; 102 } 103 104 function selectCandidate() { 105 candidates = Object.keys(interactionsByIdentifier) 106 .map(function (key) { 107 return interactionsByIdentifier[key]; 108 }) 109 .filter(function (interaction) { 110 return interaction.duration >= DURATION_THRESHOLD; 111 }) 112 .sort(function (left, right) { 113 return right.duration - left.duration; 114 }) 115 .slice(0, MAX_CANDIDATES); 116 117 if (!candidates.length) { 118 return null; 119 } 120 121 var index = Math.min(candidates.length - 1, Math.floor(interactionCount / INTERACTIONS_PER_CANDIDATE)); 122 123 return candidates[index]; 124 } 125 126 /** 127 * The longest script running while the interaction was on screen, from the 128 * long-animation-frame entries â the light equivalent of the SPA's 129 * attribution.longestScript so Razor rows carry comparable blame. Subpart 130 * classification is a web-vitals derivation the raw entries do not offer, 131 * so that one field stays SPA-only. 132 */ 133 function findLongestScript(interaction) { 134 var windowStart = interaction.startTime; 135 var windowEnd = interaction.startTime + interaction.duration; 136 var longest = null; 137
138 longAnimationFrames.forEach(function (frame) { 139 if (frame.startTime >= windowEnd || frame.startTime + frame.duration <= windowStart) { 140 return; 141 } 142 143 (frame.scripts || []).forEach(function (script) { 144 if (!longest || script.duration > longest.duration) { 145 longest = script; 146 } 147 }); 148 }); 149 150 if (!longest) { 151 return null; 152 } 153 154 var scriptStart = longest.startTime; 155 var scriptEnd = longest.startTime + longest.duration; 156 157 return { 158 duration: longest.duration, 159 intersectingDuration: Math.max(0, Math.min(scriptEnd, windowEnd) - Math.max(scriptStart, windowStart)), 160 source: longest.sourceURL || longest.invoker || '' 161 }; 162 } 163 164 function rate(value) { 165 if (value <= 200) { 166 return 'good'; 167 } 168 169 return value <= 500 ? 'needs-improvement' : 'poor'; 170 } 171 172 function round(milliseconds) { 173 return Math.round(milliseconds * 10) / 10; 174 } 175 176 function buildEnvelope(interaction) { 177 var properties = { 178 metricName: 'INP', 179 name: 'INP', 180 page_type: config.pageType, 181 route: location.pathname, 182 rating: rate(interaction.duration), 183 interactionTarget: interaction.interactionTarget, 184 interactionType: interaction.interactionType, 185 navigationType: 'navigate' 186 }; 187 188 if (config.tenantId) { 189 properties.tenantId = config.tenantId; 190 } 191 192 var connection = navigator.connection; 193 if (connection && connection.effectiveType) { 194 properties.effectiveType = connection.effectiveType; 195 } 196 197 if (typeof navigator.deviceMemory === 'number') { 198 properties.deviceMemory = String(navigator.deviceMemory); 199 } 200 201 var longestScript = findLongestScript(interaction); 202 if (longestScript) { 203 properties.longestScriptSource = longestScript.source; 204 } 205 206 // numeric fields go into measurements (customMeasurements) to match the SPA-side 207 // addInpAttribution rows â the same field name must land in the same bucket with the 208 // same type whichever page family reported it, or percentile queries silently skip rows 209 var measurements = { 210 value: round(interaction.duration), 211 inputDelay: round(interaction.inputDelay), 212 processingDuration: round(interaction.processingDuration), 213 presentationDelay: round(interaction.presentationDelay) 214 }; 215 216 if (longestScript) { 217 measurements.longestScriptDuration = round(longestScript.duration); 218 measurements.longestScriptIntersectingDuration = round(longestScript.intersectingDuration); 219 } 220 221 var tags = { 222 'ai.operation.name': location.pathname, 223 'ai.internal.sdkVersion': 'aw-rum:1' 224 }; 225 226 if (config.roleName) { 227 tags['ai.cloud.role'] = config.roleName; 228 } 229 230 if (config.tenantId) { 231 tags['ai.user.accountId'] = config.tenantId; 232 } 233 234 return { 235 name: 'Microsoft.ApplicationInsights.Event', 236 time: new Date().toISOString(), 237 iKey: config.instrumentationKey, 238 tags: tags, 239 data: { 240 baseType: 'EventData', 241 baseData: { 242 ver: 2, 243 name: 'WebVital.INP', 244 properties: properties, 245 measurements: measurements 246 } 247 } 248 }; 249 } 250 251 function report() { 252 if (reported) { 253 return; 254 } 255 256 var interaction = selectCandidate(); 257 if (!interaction) { 258 return; 259 } 260 261 reported = true; 262 navigator.sendBeacon(config.ingestionEndpoint, JSON.stringify(buildEnvelope(interaction))); 263 } 264 265 try { 266 // buffered:true picks up interactions that happened before this script ran, 267 // which for a deferred script on a server-rendered page is most of them. 268 new PerformanceObserver(function (list) { 269 list.getEntries().forEach(recordEntry); 270 }).observe({ type: 'event', buffered: true, durationThreshold: DURATION_THRESHOLD }); 271 } catch (error) { 272 return; 273 } 274 275 try { 276 new PerformanceObserver(function (list) { 277 longAnimationFrames = longAnimationFrames.concat(list.getEntries()).slice(-50); 278 }).observe({ type: 'long-animation-frame', buffered: true });
279 } catch (error) { 280 // long-animation-frame unsupported â rows simply omit the longest-script fields. 281 } 282 283 // A back/forward-cache restore resumes this same script instance rather than 284 // re-running it, so the collector starts a fresh reporting cycle for the 285 // restored pageview â mirroring web-vitals' own pageshow reset. 286 window.addEventListener('pageshow', function (event) { 287 if (event.persisted) { 288 interactionsByIdentifier = {}; 289 candidates = []; 290 interactionCount = 0; 291 reported = false; 292 } 293 }); 294 295 // visibilitychange->hidden is the last point a browser reliably runs script; 296 // pagehide covers the bfcache path where visibilitychange may not fire. 297 document.addEventListener('visibilitychange', function () { 298 if (document.visibilityState === 'hidden') { 299 report(); 300 } 301 }); 302 303 window.addEventListener('pagehide', report); 304})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.