better otel i think
This commit is contained in:
1 parent
6edbdc340d
commit
8f670dfa2f
16 files changed
+1129
-675
No files matched your search
+101
-80
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
var webVitals=function(t){"use strict";class n{t;o=0;i=[];l(t){if(t.hadRecentInput)return;const n=this.i[0],e=this.i.at(-1);this.o&&n&&e&&t.startTime-e.startTime<1e3&&t.startTime-n.startTime<5e3?(this.o+=t.value,this.i.push(t)):(this.o=t.value,this.i=[t]),this.t?.(t)}}const e=()=>{const t=performance.getEntriesByType("navigation")[0];if(t&&t.responseStart>0&&t.responseStart<performance.now())return t},o=t=>{if("loading"===document.readyState)return"loading";const n=e();if(n){if(t<n.domInteractive)return"loading";if(0===n.domContentLoadedEventStart||t<n.domContentLoadedEventStart)return"dom-interactive";if(0===n.domComplete||t<n.domComplete)return"dom-content-loaded"}return"complete"},i=t=>{const n=t.nodeName;return 1===t.nodeType?n.toLowerCase():n.toUpperCase().replace(/^#/,"")},a=t=>{let n="";try{for(;9!==t?.nodeType;){const e=t,o=e.id?"#"+e.id:[i(e),...Array.from(e.classList??[]).sort()].join(".");if(n.length+o.length>99)return n||o;if(n=n?o+">"+n:o,e.id)break;t=e.parentNode}}catch{}return n},r=new WeakMap;function s(t,n){let e=r.get(n);return e||(e=new WeakMap,r.set(n,e)),e.get(t)||e.set(t,new n),e.get(t)}let c=-1;const f=()=>c,l=t=>{addEventListener("pageshow",n=>{n.persisted&&(c=n.timeStamp,t(n))},!0)},u=(t,n,e,o)=>{let i,a;return r=>{n.value>=0&&(r||o)&&(a=n.value-(i??0),(a||void 0===i)&&(i=n.value,n.delta=a,n.rating=((t,n)=>t>n[1]?"poor":t>n[0]?"needs-improvement":"good")(n.value,e),t(n)))}},d=t=>{requestAnimationFrame(()=>requestAnimationFrame(()=>t()))},h=()=>e()?.activationStart??0;let g=-1;const v=new Set,p=()=>"hidden"!==document.visibilityState||document.prerendering?1/0:0,m=t=>{if("hidden"===document.visibilityState){if("visibilitychange"===t.type)for(const t of v)t();isFinite(g)||(g="visibilitychange"===t.type?t.timeStamp:0,removeEventListener("prerenderingchange",m,!0))}},y=(t=!1)=>{if(t&&(g=1/0),g<0){const t=h(),n=document.prerendering?void 0:globalThis.performance.getEntriesByType("visibility-state").find(n=>"hidden"===n.name&&n.startTime>=t)?.startTime;g=n??p(),addEventListener("visibilitychange",m,!0),addEventListener("prerenderingchange",m,!0),l(()=>{setTimeout(()=>{g=p()})})}return{get firstHiddenTime(){return g},onHidden(t){v.add(t)}}},b=(t,n=-1,o,i=0,a,r,s)=>{const c=e(),l=c?.navigationId||0;let u="navigate";o?u=o:f()>=0?u="back-forward-cache":c&&(document.prerendering||h()>0?u="prerender":document.wasDiscarded?u="restore":c.type&&(u=c.type.replace(/_/g,"-")));return{name:t,value:n,rating:"good",delta:0,entries:[],id:`v6-${Date.now()}-${Math.floor(8999999999999*Math.random())+1e12}`,navigationType:u,navigationId:i||l,navigationInteractionId:a,navigationURL:r||c?.name,navigationStartTime:s||0}},M=(t,n,e={})=>{try{const o=t.filter(t=>PerformanceObserver.supportedEntryTypes.includes(t));if(o.length>0){const t=new PerformanceObserver(t=>{queueMicrotask(()=>{const e=t.getEntries();o.length>1&&e.sort((t,n)=>t.startTime+t.duration-(n.startTime+n.duration)),n(e)})});for(const n of o)t.observe({type:n,buffered:!0,...e});return t}}catch{}},T=t=>globalThis.PerformanceObserver?.supportedEntryTypes?.includes("soft-navigation")&&"function"==typeof globalThis.PerformanceSoftNavigation?.prototype?.getLargestInteractionContentfulPaint&&t&&t.reportSoftNavs,E=(t,n)=>{if(t.set(n.navigationId,n),t.size>2){const n=t.keys().next().value;void 0!==n&&t.delete(n)}},D=t=>{let n=!1;return()=>{n||(t(),n=!0)}};class w{u}const S=t=>{document.prerendering?addEventListener("prerenderingchange",t,!0):t()},k=[1800,3e3],L=(t,n={})=>{const e=T(n);S(()=>{const o=s(n,w),i=y();let a,r=b("FCP");const c=M(["paint"],t=>{for(const n of t)"first-contentful-paint"===n.name&&(c.disconnect(),n.startTime<i.firstHiddenTime&&(r.value=Math.max(n.startTime-h(),0),r.entries.push(n),r.navigationId=n.navigationId||r.navigationId,a(!0)))});if(c&&(a=u(t,r,k,n.reportAllChanges),l(e=>{r=b("FCP",-1,"back-forward-cache",r.navigationId,r.navigationInteractionId,r.navigationURL,f()),a=u(t,r,k,n.reportAllChanges),d(()=>{r.value=performance.now()-e.timeStamp,a(!0)})})),e){M(["soft-navigation"],e=>{e.forEach(e=>{o.u&&e.navigationId&&E(o.u,e);const i=Math.max((e.presentationTime||e.paintTime||0)-e.startTime,0);r=b("FCP",i,"soft-navigation",e.navigationId,e.interactionId,e.name,e.startTime),a=u(t,r,k,n.reportAllChanges),a(!0)})},n)}})},P=[.1,.25],F=t=>t.find(t=>1===t.node?.nodeType)||t[0];let _=0,C=1/0,I=0;const B=t=>{for(const n of t)n.interactionId&&(C=Math.min(C,n.interactionId),I=Math.max(I,n.interactionId),_=I?(I-C)/7+1:0)};let O;const j=()=>O?_:performance.interactionCount??0,A=()=>{"interactionCount"in performance||O||(O=M(["event"],B,{durationThreshold:0}))};class N{h=0;v=[];p=new Map;m;M;T(){return j()-this.h}D(){this.h=j(),this.v.length=0,this.p.clear()}S(t){const n=this.T(),e=Math.min(this.v.length-1,Math.floor(n/50));return!n||-1!==e||"soft-navigation"!==t&&"back-forward-cache"!==t?this.v[e]:{k:8,id:-1,entries:[]}}l(t){if(this.m?.(t),!t.interactionId)return;const n=this.v.at(-1);let e=this.p.get(t.interactionId);if(e||this.v.length<10||t.Line truncated
|
||||
Reference in new issue
Block a user