PageSourceSearch

https://connecteam.com/wp-content/themes/connecteam/assets/js/web-vitals-collector.js?ver=0lj6bc

js connecteam.com collected 2026-10-01 10:31:11 UTC 7,755 bytes, 212 lines download raw bytes

1/**
2 * Core Web Vitals (RUM) collector for connecteam.com.
3 *
4 * Captures the real-user Core Web Vitals — LCP, INP, CLS, and TTFB — using the
5 * official Google `web-vitals` library (loaded first as `window.webVitals`) and
6 * beacons them to the existing BIEngine performance-ingestion endpoint
7 * `POST https://bi.connecteam.com/bi/api/Performance/Web/`.
8 *
9 * The payload conforms to the backend's ReportHitsBody contract:
10 *   { metadata: { sessionId, clientId, visitId? }, events: [ { type, hit } ] }
11 * where sessionId/clientId are the cta first-party UUIDs (reused from window.cta),
12 * `type` is "eventHit", and each `hit` carries { label, value, category, ... }.
13 *
14 * Vitals are only final at page hide, so we buffer readings and flush once via
15 * navigator.sendBeacon on the first `visibilitychange -> hidden` and on `pagehide`
16 * (fire-and-forget). Downstream, BIEngine publishes to the `performance_event_hit`
17 * topic and (companion matrix change) increments Prometheus histograms.
18 */
19(function (window, document) {
20  'use strict';
21
22  var wv = window.webVitals;
23  // Bail quietly if the web-vitals lib or the required browser APIs are missing.
24  if (!wv || !navigator || typeof navigator.sendBeacon !== 'function') {
25    return;
26  }
27
28  var ENDPOINT = 'https://bi.connecteam.com/bi/api/Performance/Web/';
29  var CATEGORY = 'web_vital';
30  var MAX_BEACON_BYTES = 60 * 1024; // stay comfortably under the server's 70 KB cap
31
32  // Latest reading per metric name (LCP/INP/CLS/TTFB), keyed so re-reports overwrite.
33  var latest = {};
34  // metric.id values already beaconed, so a second flush never double-sends a value.
35  var sentIds = {};
36
37  // Canonical UUID (any version). The cta first-party ids are v4; reject anything malformed so
38  // we attach clean attribution or none at all.
39  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
40  function isUuid(v) {
41    return typeof v === 'string' && UUID_RE.test(v);
42  }
43
44  function readIdentity() {
45    var cta = window.cta || {};
46    function safe(fn) {
47      try {
48        return typeof fn === 'function' ? fn.call(cta) : null;
49      } catch (e) {
50        return null;
51      }
52    }
53    return {
54      clientId: safe(cta.clientId),
55      sessionId: safe(cta.sessionId),
56      visitId: safe(cta.visitId),
57    };
58  }
59
60  // Extract a small, useful attribution target (CSS selector) when the lib provides one.
61  function attributionTarget(metric) {
62    var a = metric.attribution;
63    if (!a) {
64      return undefined;
65    }
66    return a.element || a.largestShiftTarget || a.interactionTarget || undefined;
67  }
68
69  function onVital(metric) {
70    // CLS is a unitless score (~0.05); the timing vitals are milliseconds.
71    var value = metric.name === 'CLS' ? metric.value.toFixed(4) : String(Math.round(metric.value));
72
73    var extra = {
74      rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
75      metric_id: metric.id,
76      navigationType: metric.navigationType,
77    };
78    var target = attributionTarget(metric);
79    if (target) {
80      extra.target = String(target).slice(0, 200);
81    }
82
83    latest[metric.name] = {
84      type: 'eventHit',
85      hit: {
86        label: metric.name, // LCP | INP | CLS | TTFB
87        value: value, // string — matches PerformanceEventModel.value
88        category: CATEGORY,
89        clientHitStartTime: Date.now(),
90        host: window.location.href,
91        pageTitle: document.title,
92        extra: extra,
93      },
94    };
95  }
96
97  function flush() {
98    var events = [];
99    for (var name in latest) {
100      if (!latest.hasOwnProperty(name)) {
101        continue;
102      }
103      var ev = latest[name];
104      var id = ev.hit.extra && ev.hit.extra.metric_id;
105      if (id && sentIds[id]) {
106        continue; // already sent this exact measurement
107      }
108      events.push(ev);
109    }
110    if (!events.length) {
111      return;
112    }
113
114    // sessionId/clientId are best-effort attribution only — the Core Web Vital (label/value/host)
115    // is the payload that matters, and the ingest endpoint tolerates null/absent identity (it
116    // coerces malformed ids to null; see matrix ReportHitsMetadata). So we send the vital
117    // regardless of identity, and attach only ids that are well-formed UUIDs — dropping a
118    // missing/garbage id client-side keeps the payload clean instead of relying on the server.
119    var identity = readIdentity();
120    var metadata = {};
121    if (isUuid(identity.sessionId)) {
122      metadata.sessionId = identity.sessionId;
123    }
124    if (isUuid(identity.clientId)) {
125      metadata.clientId = identity.clientId;
126    }
127    if (isUuid(identity.visitId)) {
128      metadata.visitId = identity.visitId;
129    }
130
131    var body;
132    try {
133      body = JSON.stringify({ metadata: metadata, events: events });
134    } catch (e) {
135      return;
136    }
137    if (body.length > MAX_BEACON_BYTES) {
138      return;
139    }
140
141    var ok = false;
142    try {
143      // Send with NO Content-Type header (empty-type Blob), NOT application/json.
144      // application/json is not CORS-safelisted, so a cross-origin POST (connecteam.com
145      // -> bi.connecteam.com) triggers a preflight that frequently cannot complete during
146      // visibilitychange/pagehide unload -- the beacon then reports success locally while
147      // the request never reaches the server, silently losing RUM.
148      //
149      // We must NOT switch to text/plain either: the ingest endpoint is FastAPI
150      // (body: ReportHitsBody = Body(...)), which only parses the body as JSON when
151      // Content-Type is application/json (or +json) OR *absent* -- text/plain yields a 422.
152      // An empty-type Blob makes the browser omit Content-Type entirely, which threads the
153      // needle: absent Content-Type => CORS "simple request" (no preflight) AND FastAPI
154      // still parses the body as JSON. No server change required. (Validated against
155      // fastapi==0.121.2, the version this endpoint runs.)
156      ok = navigator.sendBeacon(ENDPOINT, new Blob([body]));
157    } catch (e) {
158      ok = false;
159    }
160    // Fallback for when sendBeacon is unavailable or its queue is full: a keepalive fetch
161    // survives document unload. Same empty-type Blob body (no Content-Type, no preflight);
162    // mode:'no-cors' keeps the opaque response, which is fine for fire-and-forget.
163    if (!ok && typeof fetch === 'function') {
164      try {
165        fetch(ENDPOINT, {
166          method: 'POST',
167          body: new Blob([body]),
168          keepalive: true,
169          mode: 'no-cors',
170        });
171        ok = true;
172      } catch (e) {
173        ok = false;
174      }
175    }
176    if (ok) {
177      // Mark these measurements sent so the pagehide flush doesn't repeat them.
178      for (var i = 0; i < events.length; i++) {
179        var mid = events[i].hit.extra && events[i].hit.extra.metric_id;
180        if (mid) {
181          sentIds[mid] = true;
182        }
183      }
184    }
185  }
186
187  // Register the vitals. reportAllChanges defaults to false, so each fires once with
188  // its final value (web-vitals uses buffered PerformanceObserver entries, so being
189  // loaded late/deferred still captures LCP/CLS from before this script ran).
190  try {
191    wv.onLCP(onVital);
192    wv.onCLS(onVital);
193    wv.onINP(onVital);
194    wv.onTTFB(onVital);
195  } catch (e) {
196    return;
197  }
198
199  // web-vitals finalises its metrics on visibility hidden (its listener runs before
200  // ours, since it was registered first above), then we beacon the batch. pagehide
201  // is a belt-and-suspenders flush for bfcache/unload; sentIds prevents duplicates.
202  document.addEventListener(
203    'visibilitychange',
204    function () {
205      if (document.visibilityState === 'hidden') {
206        flush();
207      }
208    },
209    { capture: true }
210  );
211  window.addEventListener('pagehide', flush, { capture: true });
212})(window, document);

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.