PageSourceSearch

https://cdn.pulsetic.com/rum.js

js pulsetic.com collected 2026-10-03 21:31:20 UTC 10,235 bytes, 339 lines download raw bytes

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.