better otel i think
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m26s

This commit is contained in:
mats committed 2026-09-19 19:14:51 +02:00
1 parent 6edbdc340d
commit 8f670dfa2f
16 files changed
+1129 -675

No files matched your search

+101 -80
View File
@@ -3,9 +3,10 @@
*
* Captures uncaught errors, unhandled promise rejections, failed resource
* loads, failing HTTP requests and Web Vitals (LCP/INP/CLS/TTFB/FCP), and
* ships them as OTLP log records to this server's existing /v1/logs endpoint
* ships errors as OTLP log records to this server's existing /v1/logs endpoint
* (no separate ingestion path needed - the backend's issue view picks these
* up like any other error log).
* up like any other error log). Web Vitals go out separately: one beacon per
* page view to /v1/vitals, measured with the vendored web-vitals library.
*
* Also exposes a `traceHeaders()` helper so outgoing `fetch`/XHR calls can
* carry a W3C `traceparent` header. If the backend service is itself
@@ -114,11 +115,18 @@
endpoint = baseUrl + '/' + endpoint.replace(/^\/+/, '');
}
var vitalsEndpoint = config.vitalsEndpoint || (scriptEl && scriptEl.getAttribute('data-vitals-endpoint')) || baseUrl + '/v1/vitals';
// The web-vitals build lives next to this script; fall back to the collector's /static.
var vitalsLibUrl =
config.vitalsLibUrl ||
(scriptEl && scriptEl.getAttribute('data-vitals-lib')) ||
(scriptEl && scriptEl.src ? scriptEl.src.replace(/[^\/]*$/, '') + 'web-vitals.attribution.iife.js' : baseUrl + '/static/web-vitals.attribution.iife.js');
function isCollectorUrl(u) {
if (!u) return false;
var s = String(u);
if (endpoint && s.indexOf(endpoint) === 0) return true;
if (s.indexOf('/v1/logs') !== -1 || s.indexOf('/v1/traces') !== -1 || s.indexOf('/v1/metrics') !== -1) return true;
if (s.indexOf('/v1/logs') !== -1 || s.indexOf('/v1/traces') !== -1 || s.indexOf('/v1/metrics') !== -1 || s.indexOf('/v1/vitals') !== -1) return true;
return false;
}
@@ -294,92 +302,108 @@
enqueue(buildRecord(severity, message, attrs, traceId));
}
// Web Vitals via PerformanceObserver. Values are reported as INFO logs
// tagged rum.vital=<name>; the server can chart/aggregate them like
// any other telemetry. Each vital fires once when it reaches its
// final value (plus a first-input proxy for INP on older browsers).
// Web Vitals: measured by the vendored `web-vitals` library (attribution
// build, loaded lazily so it never blocks the page) and reported as ONE
// beacon per page view to /v1/vitals when the page is hidden. The
// beacon carries every metric with its attribution (LCP element, INP
// target/breakdown, CLS shift source...). If the page is hidden again
// later (tab switch) and a metric changed - typically INP - a fuller
// snapshot with the same pageView id is sent; the newest one wins.
function observeVitals() {
if (typeof global.PerformanceObserver !== 'function') return;
var pageViewId = randomHex(16);
var vitals = {};
var navType = '';
var dirty = false;
var sampled = sampleRate >= 1 || Math.random() <= sampleRate;
function report(name, value, extra) {
var attrs = Object.assign({ 'rum.vital': name, 'rum.value': Math.round(value) }, extra || {});
// Vitals are telemetry, not errors: bypass the error rate limit
// but keep sampling.
if (sampleRate < 1 && Math.random() > sampleRate) return;
enqueue(buildRecord('INFO', name + ' ' + Math.round(value), attrs, ''));
var picks = {
LCP: ['target', 'url', 'timeToFirstByte', 'resourceLoadDelay', 'resourceLoadDuration', 'elementRenderDelay'],
INP: ['interactionTarget', 'interactionType', 'inputDelay', 'processingDuration', 'presentationDelay', 'loadState', 'totalScriptDuration', 'totalStyleAndLayoutDuration', 'totalPaintDuration'],
CLS: ['largestShiftTarget', 'largestShiftValue', 'largestShiftTime', 'loadState'],
FCP: ['timeToFirstByte', 'firstByteToFCP', 'loadState'],
TTFB: ['waitingDuration', 'cacheDuration', 'dnsDuration', 'connectionDuration', 'requestDuration']
};
// Values keep 3 decimals: CLS is a tiny fraction and must not be rounded to an integer.
function round(n) {
return typeof n === 'number' ? Math.round(n * 1000) / 1000 : n;
}
function onPaint(list) {
list.getEntries().forEach(function (entry) {
if (entry.name === 'first-contentful-paint') report('FCP', entry.startTime);
function slim(metric) {
var out = {};
var src = metric.attribution || {};
(picks[metric.name] || []).forEach(function (k) {
if (src[k] !== undefined && src[k] !== null) out[k] = round(src[k]);
});
var script = src.longestScript && src.longestScript.entry;
if (script && script.sourceURL) {
out.longestScriptUrl = script.sourceURL;
if (script.sourceFunctionName) out.longestScriptFunction = script.sourceFunctionName;
}
return out;
}
function onLCP(list) {
var entries = list.getEntries();
var last = entries[entries.length - 1];
if (last) report('LCP', last.startTime, { 'rum.element': last.element && last.element.tagName });
function onMetric(metric) {
vitals[metric.name] = { value: round(metric.value), delta: round(metric.delta), id: metric.id, attribution: slim(metric) };
navType = metric.navigationType || navType;
dirty = true;
}
function onCLS() {
var cls = 0;
var lastReport = 0;
var observer = new global.PerformanceObserver(function (list) {
list.getEntries().forEach(function (entry) {
if (!entry.hadRecentInput) cls += entry.value;
});
// Throttle: report at most once per 10s, plus a final value on
// pagehide, instead of once per layout shift.
var now = Date.now();
if (now - lastReport > 10000) {
lastReport = now;
report('CLS', Math.round(cls * 1000) / 1000);
}
clsFinal = function () {
report('CLS', Math.round(cls * 1000) / 1000);
clsFinal = null;
};
function sendVitals() {
if (!dirty || !sampled) return;
dirty = false;
var body = JSON.stringify({
service: serviceName,
environment: environment,
release: release,
session: sessionId,
pageView: pageViewId,
url: global.location ? global.location.href : '',
ua: global.navigator ? global.navigator.userAgent : '',
viewport: global.innerWidth ? global.innerWidth + 'x' + global.innerHeight : '',
nav: navType,
conn: (global.navigator && global.navigator.connection && global.navigator.connection.effectiveType) || '',
metrics: vitals
});
var url = vitalsEndpoint;
if (ingestKey) url += (url.indexOf('?') === -1 ? '?' : '&') + 'key=' + encodeURIComponent(ingestKey);
// text/plain keeps it a CORS "simple" request: no preflight, so it survives unload.
var queued = false;
try {
observer.observe({ type: 'layout-shift', buffered: true });
} catch (e) {
/* unsupported */
clsFinal = null;
}
}
function onINP(entry) {
var duration = entry.duration;
var interactionId = entry.interactionId;
if (!interactionId) return;
if (duration > (observerState.maxINP || 0)) {
observerState.maxINP = duration;
report('INP', duration);
}
}
var observerState = {};
try {
new global.PerformanceObserver(onPaint).observe({ type: 'paint', buffered: true });
} catch (e) {}
try {
new global.PerformanceObserver(onLCP).observe({ type: 'largest-contentful-paint', buffered: true });
} catch (e) {}
onCLS();
try {
new global.PerformanceObserver(function (list) {
list.getEntries().forEach(onINP);
}).observe({ type: 'event', buffered: true, durationThreshold: 40 });
} catch (e) {}
// Navigation timing: TTFB.
try {
var nav = performance.getEntriesByType('navigation')[0];
if (nav && nav.responseStart) {
report('TTFB', nav.responseStart);
}
queued =
!!(global.navigator && typeof global.navigator.sendBeacon === 'function') &&
global.navigator.sendBeacon(url, new global.Blob([body], { type: 'text/plain;charset=UTF-8' }));
} catch (e) {}
if (!queued && rawFetch) {
rawFetch.call(global, url, { method: 'POST', mode: 'cors', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, body: body, keepalive: true }).catch(function () {});
}
}
var script = global.document.createElement('script');
script.async = true;
script.src = vitalsLibUrl;
script.onload = function () {
var wv = global.webVitals;
if (!wv) return;
// INP durationThreshold stays at the library default; one listener per metric.
[wv.onLCP, wv.onINP, wv.onCLS, wv.onFCP, wv.onTTFB].forEach(function (observe) {
if (typeof observe === 'function') observe(onMetric);
});
// Registered after the library's own hidden listeners, so the
// final LCP/INP/CLS values are already collected when this runs.
global.addEventListener('visibilitychange', function () {
if (global.document.visibilityState === 'hidden') sendVitals();
});
global.addEventListener('pagehide', sendVitals);
// bfcache restore is a new page view for vitals purposes.
global.addEventListener('pageshow', function (e) {
if (e.persisted) {
pageViewId = randomHex(16);
vitals = {};
}
});
};
global.document.head.appendChild(script);
}
// traceHeaders returns a headers object carrying a fresh (or
@@ -421,7 +445,6 @@
flush(); // ensure earlier queued events don't mix contexts ambiguously
}
var contextAttrs = {};
var clsFinal = null;
function onError(event) {
// Resource load errors (img/script/link) surface as an ErrorEvent-less
@@ -631,12 +654,10 @@
// errors that happen right before navigation are not lost, and emit
// the final CLS value.
global.addEventListener('pagehide', function () {
if (clsFinal) clsFinal();
flush();
});
global.addEventListener('visibilitychange', function () {
if (global.document && global.document.visibilityState === 'hidden') {
if (clsFinal) clsFinal();
flush();
}
});