1/** 2 * perfLogger.js 3 * Best-effort page-load performance logger (Navigation Timing + Core Web Vitals). 4 * 5 * Hard rules (identical to apiLogger.js): 6 * - Never throw. Never block the main thread. Zero impact on the end user. 7 * - Passive collection only (PerformanceObserver via web-vitals, no polling). 8 * - One fire-and-forget beacon per page, flushed when the page is hidden. 9 * - Silently no-ops if logUserActivity / web-vitals are unavailable. 10 * 11 * Depends on: 12 * - window.logUserActivity (userActivityLog.js) -> sendBeacon transport 13 * - window.webVitals (web-vitals.attribution.iife.js, loaded before this) 14 * 15 * Logged shape: ('PERF_METRICS', { ...navTimings, vitals:{...}, ...deviceNet }) 16 */ 17(function (global) { 18 'use strict'; 19 try { 20 if (!global || typeof global !== 'object' || !global.performance) return; 21 22 // --- Sampling: fraction of page loads to collect. 1.0 = capture all. --- 23 var SAMPLE_RATE = 1.0; 24 if (Math.random() > SAMPLE_RATE) return; 25 26 // Accumulate vitals here as web-vitals finalizes each one. 27 var vitals = {}; 28 29 function round(n) { return (typeof n === 'number' && isFinite(n)) ? Math.round(n) : null; } 30 31 // --- (A) Navigation Timing â full fidelity, ready at page load --- 32 function collectNav() { 33 var m = {}; 34 try { 35 var nav = performance.getEntriesByType('navigation')[0]; 36 if (!nav) return m; 37 m.ttfb = round(nav.responseStart - nav.requestStart); 38 m.dns = round(nav.domainLookupEnd - nav.domainLookupStart); 39 m.tcp = round(nav.connectEnd - nav.connectStart); 40 m.tls = nav.secureConnectionStart ? round(nav.connectEnd - nav.secureConnectionStart) : 0; 41 m.req = round(nav.responseEnd - nav.requestStart); 42 m.dom_interactive = round(nav.domInteractive - nav.startTime); 43 m.dcl = round(nav.domContentLoadedEventEnd - nav.startTime); 44 m.load = round(nav.loadEventEnd - nav.startTime); 45 m.redirect_count = nav.redirectCount; 46 m.redirect_ms = round(nav.redirectEnd - nav.redirectStart); 47 m.transfer_bytes = nav.transferSize; 48 m.encoded_bytes = nav.encodedBodySize; 49 m.decoded_bytes = nav.decodedBodySize; 50 m.nav_type = nav.type; // navigate | reload | back_forward | prerender 51 m.protocol = nav.nextHopProtocol; // h2, http/1.1 ... 52 } catch (e) { /* swallow */ } 53 return m; 54 } 55 56 // --- Route + full device / network context --- 57 function context() { 58 var c = {}; 59 try { 60 if (global.location) { 61 c.path = global.location.pathname; 62 c.query = global.location.search; 63 c.hash = global.location.hash; 64 c.href = global.location.href; 65 } 66 c.ref = document.referrer || ''; 67 68 var conn = navigator.connection || {}; 69 c.net_effective = conn.effectiveType || null; // 4g / 3g / 2g / slow-2g 70 c.downlink = (typeof conn.downlink === 'number') ? conn.downlink : null; // Mbps 71 c.rtt = (typeof conn.rtt === 'number') ? conn.rtt : null; // ms 72 c.save_data = !!conn.saveData; 73 74 c.mem_gb = (typeof navigator.deviceMemory === 'number') ? navigator.deviceMemory : null; 75 c.cores = navigator.hardwareConcurrency || null; 76 c.dpr = global.devicePixelRatio || 1; 77 c.vw = global.innerWidth || null; 78 c.vh = global.innerHeight || null; 79 c.sw = (screen && screen.width) || null; 80 c.sh = (screen && screen.height) || null; 81 c.lang = navigator.language || null; 82 c.tz = (global.Intl && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : null; 83 84 // Chrome-only heap snapshot â useful for leak hunting. 85 if (performance.memory) { 86 c.js_heap_used = performance.memory.usedJSHeapSize; 87 c.js_heap_limit = performance.memory.jsHeapSizeLimit; 88 } 89
90 // Top 5 slowest resources â pinpoints the culprit asset. 91 c.slow_resources = performance.getEntriesByType('resource') 92 .map(function (r) { 93 return { name: r.name, dur: round(r.duration), size: r.transferSize || 0, type: r.initiatorType }; 94 }) 95 .sort(function (a, b) { return (b.dur || 0) - (a.dur || 0); }) 96 .slice(0, 5); 97 } catch (e) { /* swallow */ } 98 return c; 99 } 100 101 // --- (B) Core Web Vitals via web-vitals (attribution build) --- 102 function collectVitals() { 103 try { 104 var wv = global.webVitals; 105 if (!wv) return; 106 var save = function (metric) { 107 try { 108 vitals[metric.name] = { 109 value: (metric.name === 'CLS') ? +metric.value.toFixed(3) : round(metric.value), 110 rating: metric.rating, // good | needs-improvement | poor 111 attribution: metric.attribution || null 112 }; 113 } catch (e) { /* swallow */ } 114 }; 115 if (wv.onLCP) wv.onLCP(save); 116 if (wv.onINP) wv.onINP(save); 117 if (wv.onCLS) wv.onCLS(save); 118 if (wv.onTTFB) wv.onTTFB(save); 119 if (wv.onFCP) wv.onFCP(save); 120 } catch (e) { /* swallow */ } 121 } 122 123 // --- Flush ONCE when the page is going away (vitals are final only then) --- 124 var sent = false; 125 function flush() { 126 if (sent) return; 127 sent = true; 128 try { 129 var payload = context(); 130 var nav = collectNav(); 131 for (var k in nav) { if (nav.hasOwnProperty(k)) payload[k] = nav[k]; } 132 payload.vitals = vitals; 133 if (typeof global.logUserActivity === 'function') { 134 global.logUserActivity('PERF_METRICS', payload); 135 } 136 } catch (e) { /* swallow */ } 137 } 138 139 // web-vitals registers its own hidden/pagehide listeners inside these calls, 140 // so they run before ours and populate `vitals` prior to flush(). 141 collectVitals(); 142 143 // 'visibilitychange -> hidden' is the reliable end-of-page signal on all 144 // platforms (tab close, navigation, mobile app switch). 'unload' is not. 145 global.addEventListener('visibilitychange', function () { 146 if (document.visibilityState === 'hidden') flush(); 147 }); 148 global.addEventListener('pagehide', flush); // bfcache belt-and-suspenders 149 } catch (e) { /* never affect the user */ } 150})(typeof window !== 'undefined' ? window : this);
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.