1/*! 2 * Pulsetic RUM tracker 3 * Collects navigation timing + Core Web Vitals from real visitors. Sends 4 * lightweight beacons per pageview to the Pulsetic API: navigation timings 5 * once the page has loaded (powers realtime), final Core Web Vitals when the 6 * visitor leaves. Only the pageview beacon counts against the quota. 7 * 8 * Usage: 9 * <script async src="https://cdn.pulsetic.com/rum.js" data-pulsetic-rum="SITE_HASH"></script> 10 */ 11(function () { 12 'use strict' 13 14 if (window !== window.top || document.prerendering) { 15 return 16 } 17 18 var script = document.currentScript || document.querySelector('script[data-pulsetic-rum]') 19 if (!script) { 20 return 21 } 22 23 var hash = script.getAttribute('data-pulsetic-rum') 24 if (!hash) { 25 return 26 } 27 28 var base = script.getAttribute('data-endpoint') || 'https://api.pulsetic.com' 29 var endpoint = base + '/rum/beacon/' + hash 30 var errorEndpoint = base + '/rum/error/' + hash 31 32 // Keep a reference to the native fetch BEFORE we wrap it, so our own beacons 33 // never recurse through the HTTP-error interceptor below. 34 var nativeFetch = window.fetch && window.fetch.bind(window) 35 36 var pageviewSent = false 37 var vitalsSent = false 38 var vitals = { lcp: -1, cls: -1, inp: -1, fcp: -1 } 39 40 // text/plain Blob avoids a CORS preflight; keepalive fetch is the fallback. 41 function post (url, body) { 42 try { 43 if (navigator.sendBeacon) { 44 navigator.sendBeacon(url, new Blob([body], { type: 'text/plain' })) 45 } else if (nativeFetch) { 46 nativeFetch(url, { 47 method: 'POST', 48 body: body, 49 headers: { 'Content-Type': 'text/plain' }, 50 keepalive: true 51 }).catch(function () {}) 52 } 53 } catch (e) {} 54 } 55 56 // CLS: session windows (max 5s, 1s gap), report the worst session 57 var clsSessionValue = 0 58 var clsSessionFirstTs = 0 59 var clsSessionLastTs = 0 60 61 function observe (type, callback, options) { 62 try { 63 var observer = new PerformanceObserver(function (list) { 64 list.getEntries().forEach(callback) 65 }) 66 observer.observe(Object.assign({ type: type, buffered: true }, options || {})) 67 return observer 68 } catch (e) { 69 return null 70 } 71 } 72 73 observe('largest-contentful-paint', function (entry) { 74 vitals.lcp = Math.round(entry.renderTime || entry.loadTime || entry.startTime) 75 }) 76 77 observe('paint', function (entry) { 78 if (entry.name === 'first-contentful-paint') { 79 vitals.fcp = Math.round(entry.startTime) 80 } 81 }) 82 83 observe('layout-shift', function (entry) { 84 if (entry.hadRecentInput) { 85 return 86 } 87 88 if (clsSessionValue && 89 entry.startTime - clsSessionLastTs < 1000 && 90 entry.startTime - clsSessionFirstTs < 5000) { 91 clsSessionValue += entry.value 92 } else { 93 clsSessionValue = entry.value 94 clsSessionFirstTs = entry.startTime 95 } 96 97 clsSessionLastTs = entry.startTime 98 99 if (vitals.cls < 0 || clsSessionValue > vitals.cls) { 100 vitals.cls = clsSessionValue 101 } 102 }) 103 104 // Simplified INP: longest interaction observed (web-vitals uses ~p98) 105 observe('event', function (entry) { 106 if (entry.interactionId && Math.round(entry.duration) > vitals.inp) { 107 vitals.inp = Math.round(entry.duration) 108 } 109 }, { durationThreshold: 40 }) 110 111 function ms (value) { 112 return value > 0 ? Math.round(value) : 0 113 } 114 115 function timings () { 116 var nav = (performance.getEntriesByType && performance.getEntriesByType('navigation')) 117 nav = nav && nav[0] 118 119 if (!nav) { 120 return null 121 } 122 123 var tls = nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0 124 var tcpEnd = nav.secureConnectionStart > 0 ? nav.secureConnectionStart : nav.connectEnd 125 126 return { 127 dns: ms(nav.domainLookupEnd - nav.domainLookupStart), 128 tcp: ms(tcpEnd - nav.connectStart), 129 tls: ms(tls), 130 ttfb: ms(nav.responseStart - nav.startTime), 131 di: ms(nav.domInteractive - nav.startTime), 132 dcl: ms(nav.domContentLoadedEventEnd - nav.startTime), 133 load: ms(nav.loadEventEnd - nav.startTime) 134 } 135 } 136 137 function navType () { 138 var nav = (performance.getEntriesByType && performance.getEntriesByType('navigation')) 139 return (nav && nav[0] && nav[0].type) || 'navigate' 140 } 141 142 function vitalsPayload () { 143 return { 144 lcp: vitals.lcp, 145 cls: vitals.cls >= 0 ? Math.round(vitals.cls * 1000) / 1000 : -1, 146 inp: vitals.inp, 147 fcp: vitals.fcp 148 } 149 } 150 151 // includeVitals=true is the left-before-load fallback: one combined beacon 152 // (timings + current vitals) and no separate vitals beacon afterwards. 153 function sendPageview (includeVitals) { 154 if (pageviewSent) { 155 return 156 } 157 pageviewSent = true 158 159 if (includeVitals) { 160 vitalsSent = true 161 } 162 163 var connection = navigator.connection || {} 164 165 post(endpoint, JSON.stringify({ 166 e: 'pageview', 167 u: window.location.href, 168 r: document.referrer || '', 169 w: (window.screen && window.screen.width) || 0, 170 h: (window.screen && window.screen.height) || 0, 171 ct: connection.effectiveType || '', 172 nt: navType(), 173 t: timings() || {}, 174 // Always carry v (all -1 here): the API drops beacons whose t AND v are 175 // both empty, and t can be empty on browsers without navigation timing. 176 v: includeVitals ? vitalsPayload() : { lcp: -1, cls: -1, inp: -1, fcp: -1 } 177 })) 178 } 179 180 // Final Core Web Vitals ride a second, unbilled beacon when the visitor 181 // leaves â CLS/INP accumulate over the page's life and LCP can grow until 182 // the first interaction, so they are only trustworthy at leave time. 183 function sendVitals () { 184 if (!pageviewSent || vitalsSent) { 185 return 186 } 187 vitalsSent = true 188 189 post(endpoint, JSON.stringify({ 190 e: 'vitals', 191 u: window.location.href, 192 v: vitalsPayload() 193 })) 194 } 195 196 function onLeave () { 197 if (!pageviewSent) { 198 sendPageview(true) 199 } else { 200 sendVitals() 201 } 202 } 203 204 function onLoaded () { 205 // loadEventEnd is only populated once the load event finishes dispatching. 206 setTimeout(function () { 207 sendPageview(false) 208 }, 60) 209 } 210 211 if (document.readyState === 'complete') { 212 onLoaded() 213 } else { 214 window.addEventListener('load', onLoaded) 215 } 216 217 document.addEventListener('visibilitychange', function () { 218 if (document.visibilityState === 'hidden') { 219 onLeave() 220 } 221 }) 222 223 window.addEventListener('pagehide', onLeave) 224 225 // ---- Error tracking: uncaught JS errors + failed HTTP requests ---- 226 // Errors are sent immediately (not batched with the pageview beacon): a hard 227 // crash can prevent pagehide from firing. A per-page cap plus client-side 228 // dedup keep a broken page from flooding the quota. 229 var MAX_ERRORS = 10 230 var errCount = 0 231 var errSeen = {} 232 233 function trunc (value, max) { 234 value = value == null ? '' : String(value) 235 return value.length > max ? value.slice(0, max) : value 236 } 237 238 function sendError (payload, dedupKey) { 239 try { 240 if (errCount >= MAX_ERRORS || errSeen[dedupKey]) { 241 return 242 } 243 errSeen[dedupKey] = true 244 errCount++ 245 payload.u = window.location.href 246 post(errorEndpoint, JSON.stringify(payload)) 247 } catch (e) {} 248 } 249 250 function jsError (message, filename, lineno, colno, stack) { 251 sendError({ 252 k: 'js', 253 m: trunc(message, 1000), 254 f: trunc(filename, 1024), 255 l: lineno || 0, 256 c: colno || 0, 257 s: trunc(stack, 4000) 258 }, 'js|' + message + '|' + filename + '|' + lineno) 259 } 260 261 window.addEventListener('error', function (event) { 262 // Ignore resource load errors (img/script/link) â they carry no script error. 263 if (!event.message && !event.error) { 264 return 265 } 266 var err = event.error 267 jsError(event.message, event.filename, event.lineno, event.colno, err && err.stack) 268 }) 269 270 window.addEventListener('unhandledrejection', function (event) { 271 var reason = event.reason
272 var message = reason && reason.message ? reason.message : String(reason) 273 jsError('Unhandled rejection: ' + message, '', 0, 0, reason && reason.stack) 274 }) 275 276 function httpError (method, url, status) { 277 try { 278 // Never report our own beacons (avoids recursion / self-noise). 279 if (!url || String(url).indexOf(base) === 0) { 280 return 281 } 282 var a = document.createElement('a') 283 a.href = url 284 var clean = a.protocol + '//' + a.host + a.pathname 285 sendError({ 286 k: 'http', 287 st: status || 0, 288 hm: trunc(method || 'GET', 10).toUpperCase(), 289 ru: trunc(clean, 1024) 290 }, 'http|' + method + '|' + status + '|' + a.host + a.pathname) 291 } catch (e) {} 292 } 293 294 // Wrap fetch: always call and return the original; record >=400 + network failures. 295 if (nativeFetch) { 296 window.fetch = function () { 297 var args = arguments 298 var input = args[0] 299 var url = (input && input.url) ? input.url : String(input) 300 var method = (args[1] && args[1].method) || (input && input.method) || 'GET' 301 var promise = nativeFetch.apply(this, args) 302 303 try { 304 return promise.then(function (res) { 305 // Opaque (no-cors) responses report status 0 but are not errors. 306 if (res && res.type !== 'opaque' && res.status >= 400) { 307 httpError(method, url, res.status) 308 } 309 return res 310 }, function (err) { 311 httpError(method, url, 0) 312 throw err 313 }) 314 } catch (e) { 315 return promise 316 } 317 } 318 } 319 320 // Wrap XHR: stash method/url on open, read status on loadend via a listener 321 // (not a handler overwrite) so the customer's own handlers keep working. 322 if (window.XMLHttpRequest && XMLHttpRequest.prototype) { 323 var xhrOpen = XMLHttpRequest.prototype.open 324 XMLHttpRequest.prototype.open = function (method, url) { 325 try { 326 this.__rumMethod = method 327 this.__rumUrl = url 328 this.addEventListener('loadend', function () { 329 // status 0 = network failure / aborted / CORS-blocked 330 if (this.status === 0 || this.status >= 400) { 331 httpError(this.__rumMethod, this.__rumUrl, this.status) 332 } 333 }) 334 } catch (e) {} 335 336 return xhrOpen.apply(this, arguments) 337 } 338 } 339})()
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.