1/** 2 * Core Web Vitals RUM collector. A plain static script (outside the app 3 * bundle) so measuring survives failed hydration or unavailable GTM. 4 * Config comes from <meta name="web-vitals-config">; no meta or 5 * sampleRate <= 0 means it does nothing. No cookies, storage or user ids. 6 */ 7(function initWebVitals() { 8 "use strict"; 9 10 var CONFIG_SELECTOR = 'meta[name="web-vitals-config"]'; 11 // Keep beacons well below the ~64 kB sendBeacon limit browsers enforce 12 // (measured in bytes, not string length - selectors may be non-ASCII). 13 var MAX_PAYLOAD_BYTES = 32000; 14 var MAX_STRING_LENGTH = 200; 15 var MAX_URL_LENGTH = 300; 16 var MAX_QUEUE_LENGTH = 50; 17 18 // Attribution fields forwarded per metric. An explicit allow list keeps 19 // the payload small and guarantees no unexpected field (element text, 20 // full URLs with query strings) ever leaves the browser. 21 var ATTRIBUTION_FIELDS = { 22 CLS: ["largestShiftTarget", "largestShiftValue", "loadState"], 23 FCP: ["timeToFirstByte", "firstByteToFCP", "loadState"], 24 INP: [ 25 "interactionTarget", 26 "interactionType", 27 "inputDelay", 28 "processingDuration", 29 "presentationDelay", 30 "totalScriptDuration", 31 "loadState", 32 ], 33 LCP: [ 34 "target", 35 "url", 36 "timeToFirstByte", 37 "resourceLoadDelay", 38 "resourceLoadDuration", 39 "elementRenderDelay", 40 ], 41 TTFB: [ 42 "waitingDuration", 43 "cacheDuration", 44 "dnsDuration", 45 "connectionDuration", 46 "requestDuration", 47 ], 48 }; 49 50 var configMeta = document.querySelector(CONFIG_SELECTOR); 51 52 if (!configMeta) { 53 return; 54 } 55 56 var config; 57 58 try { 59 config = JSON.parse(configMeta.getAttribute("content") || ""); 60 // eslint-disable-next-line @typescript-eslint/no-unused-vars -- optional catch binding is ES2019; a bare catch would make the whole ES5 script unparsable in legacy browsers 61 } catch (parseError) { 62 return; 63 } 64 65 if (!config || !config.endpoint || !config.libSrc) { 66 return; 67 } 68 69 var sampleRate = Number(config.sampleRate); 70 71 // One sampling decision per page load, so a sampled-in page reports all 72 // of its metrics and a sampled-out page does not even fetch the library. 73 if (!(sampleRate > 0) || Math.random() >= sampleRate) { 74 return; 75 } 76 77 var queue = []; 78 var batchIndex = 0; 79 var pageloadId = createPageloadId(); 80 81 function createPageloadId() { 82 // crypto.randomUUID is unavailable outside secure contexts. 83 if (window.crypto && typeof window.crypto.randomUUID === "function") { 84 return window.crypto.randomUUID(); 85 } 86 87 return ( 88 Date.now().toString(36) + 89 "-" + 90 Math.random().toString(36).slice(2, 12) 91 ); 92 } 93 94 function truncate(value, maxLength) { 95 if (typeof value !== "string") { 96 return null; 97 } 98 99 return value.length > maxLength ? value.slice(0, maxLength) : value; 100 } 101 102 function byteLength(text) { 103 // Blob measures UTF-8 bytes; falls back to string length where 104 // Blob is unavailable (never in browsers this script targets). 105 return typeof Blob === "function" ? new Blob([text]).size : text.length; 106 } 107 108 function roundValue(value) { 109 // Sub-millisecond precision is noise; CLS needs decimals, so keep 110 // four of them. 111 return Math.round(value * 10000) / 10000; 112 } 113 114 function stripQuery(value) { 115 return typeof value === "string" ? value.split("?")[0] : value; 116 } 117 118 function sanitizeAttributionValue(key, value) { 119 if (typeof value === "number") { 120 return isFinite(value) ? roundValue(value) : null; 121 } 122 123 if (typeof value === "string") { 124 // URLs lose their query strings - they may carry signed tokens 125 // or campaign parameters and add nothing to diagnostics. 126 return key === "url" 127 ? truncate(stripQuery(value), MAX_URL_LENGTH) 128 : truncate(value, MAX_STRING_LENGTH); 129 } 130 131 return null; 132 } 133 134 function pickAttribution(metric) { 135 var source = metric.attribution; 136 var fields = ATTRIBUTION_FIELDS[metric.name]; 137 138 if (!source || !fields) { 139 return null; 140 } 141 142 var result = {}; 143 var hasValue = false;
144 145 for (var i = 0; i < fields.length; i++) { 146 var key = fields[i]; 147 var value = sanitizeAttributionValue(key, source[key]); 148 149 if (value !== null) { 150 result[key] = value; 151 hasValue = true; 152 } 153 } 154 155 // The slowest script behind a bad INP - this is what points at 156 // third-party tags (GTM and friends) as the actual cost. 157 var longestScript = source.longestScript; 158 159 if (longestScript && longestScript.entry) { 160 var scriptUrl = truncate( 161 stripQuery(longestScript.entry.sourceURL), 162 MAX_URL_LENGTH 163 ); 164 165 if (scriptUrl) { 166 result.longestScriptUrl = scriptUrl; 167 hasValue = true; 168 } 169 170 if (typeof longestScript.intersectingDuration === "number") { 171 result.longestScriptDuration = roundValue( 172 longestScript.intersectingDuration 173 ); 174 hasValue = true; 175 } 176 177 if (typeof longestScript.subpart === "string") { 178 result.longestScriptSubpart = longestScript.subpart; 179 hasValue = true; 180 } 181 } 182 183 return hasValue ? result : null; 184 } 185 186 function getBuildId() { 187 // Production builds use the git hash as the Next.js build id 188 // (generateBuildId in next.config), which makes before/after 189 // deploy comparisons possible without any extra configuration. 190 var nextData = window.__NEXT_DATA__; 191 192 if (nextData && typeof nextData.buildId === "string") { 193 return truncate(nextData.buildId, 40); 194 } 195 196 return null; 197 } 198 199 function getPageType() { 200 // Route pattern (for example /catalog/[id]) groups metrics by 201 // template. The Next.js client router stays current across 202 // client-side navigation, __NEXT_DATA__ only holds the entry route. 203 var nextGlobal = window.next; 204 205 if ( 206 nextGlobal && 207 nextGlobal.router && 208 typeof nextGlobal.router.pathname === "string" 209 ) { 210 return truncate(nextGlobal.router.pathname, MAX_STRING_LENGTH); 211 } 212 213 var nextData = window.__NEXT_DATA__; 214 215 if (nextData && typeof nextData.page === "string") { 216 return truncate(nextData.page, MAX_STRING_LENGTH); 217 } 218 219 return null; 220 } 221 222 function getConnection() { 223 var connection = 224 navigator.connection || 225 navigator.mozConnection || 226 navigator.webkitConnection; 227 228 return connection || null; 229 } 230 231 function buildDevice() { 232 var connection = getConnection(); 233 234 return { 235 viewportW: window.innerWidth || null, 236 deviceMemory: 237 typeof navigator.deviceMemory === "number" 238 ? navigator.deviceMemory 239 : null, 240 effectiveType: connection 241 ? truncate(connection.effectiveType, 20) 242 : null, 243 saveData: connection ? !!connection.saveData : null, 244 }; 245 } 246 247 function handleMetric(metric) { 248 if (queue.length >= MAX_QUEUE_LENGTH) { 249 return; 250 } 251 252 queue.push({ 253 name: metric.name, 254 id: metric.id, 255 value: roundValue(metric.value), 256 delta: roundValue(metric.delta), 257 rating: metric.rating || null, 258 navigationType: metric.navigationType || null, 259 source: "web-vitals", 260 // Captured when the metric is reported, not when it is sent - 261 // CLS and INP are only finalized as the page is hidden, which 262 // may happen on a different route than the one that was loaded. 263 pathname: truncate(location.pathname, MAX_STRING_LENGTH), 264 pageType: getPageType(), 265 attribution: pickAttribution(metric), 266 }); 267 } 268 269 function send(body) { 270 if ( 271 navigator.sendBeacon && 272 navigator.sendBeacon(config.endpoint, body) 273 ) { 274 return; 275 } 276 277 // sendBeacon refuses oversized or queued-up payloads; keepalive is 278 // the only fetch mode that survives an unloading document. 279 if (typeof fetch === "function") { 280 fetch(config.endpoint, { 281 body: body, 282 method: "POST", 283 keepalive: true, 284 credentials: "omit", 285 }).catch(function ignoreDeliveryFailure() {}); 286 } 287 } 288 289 function flush() { 290 if (queue.length === 0) { 291 return; 292 } 293 294 var metrics = queue.splice(0, queue.length); 295 var payload = { 296 // Stable per delivery, so a retried beacon is deduplicated by 297 // BigQuery while a genuine second flush is kept. 298 batchId: pageloadId + ":" + batchIndex++, 299 pageloadId: pageloadId, 300 brand: config.brand, 301 site: config.site || null, 302 locale: config.locale || null, 303 appVersion: getBuildId(), 304 sampling: sampleRate, 305 gtmLoaded: !!window.google_tag_manager, 306 device: buildDevice(), 307 abTests: config.abTests || [], 308 metrics: metrics, 309 }; 310 var body = JSON.stringify(payload); 311 312 if (byteLength(body) > MAX_PAYLOAD_BYTES) { 313 // Attribution is the bulk of the payload; better to deliver the 314 // metric values without diagnostics than to lose the batch. 315 for (var i = 0; i < metrics.length; i++) { 316 metrics[i].attribution = null; 317 } 318 body = JSON.stringify(payload); 319 320 if (byteLength(body) > MAX_PAYLOAD_BYTES) { 321 return; 322 } 323 } 324 325 send(body); 326 } 327 328 function registerMetrics(webVitals) { 329 webVitals.onCLS(handleMetric); 330 webVitals.onINP(handleMetric); 331 webVitals.onLCP(handleMetric); 332 webVitals.onFCP(handleMetric); 333 webVitals.onTTFB(handleMetric); 334 335 // Registered only after the library has its own listeners in place. 336 // web-vitals finalizes CLS and INP from a capture-phase 337 // visibilitychange listener on window, so a bubble-phase listener 338 // added here always runs after the metrics have been queued. 339 // 340 // Deliberately NO pagehide listener: during unload pagehide fires 341 // BEFORE visibilitychange, so flushing there would send a premature 342 // partial beacon and the finalized CLS/INP in a second one. All
343 // browsers this script measures (visibilitychange has been fired on 344 // navigation since Chrome 68 / Safari 14.5) reach this handler. 345 addEventListener("visibilitychange", function onVisibilityChange() { 346 if (document.visibilityState === "hidden") { 347 flush(); 348 } 349 }); 350 } 351 352 function loadLibrary() { 353 var script = document.createElement("script"); 354 355 script.src = config.libSrc; 356 script.async = true; 357 358 script.onload = function onLibraryLoad() { 359 if (window.webVitals) { 360 registerMetrics(window.webVitals); 361 } 362 }; 363 364 document.head.appendChild(script); 365 } 366 367 // The library reads buffered performance entries, so loading it late 368 // costs no data while keeping it off the critical path. 369 loadLibrary(); 370})();
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.