complete performance and reliability follow-ups
This commit is contained in:
@@ -22,7 +22,7 @@ export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
|
||||
function send(name, value) {
|
||||
var body = JSON.stringify({ name: name, value: value, rating: rating(name, value), route: location.pathname, navigationType: performance.getEntriesByType("navigation")[0]?.type });
|
||||
if (navigator.sendBeacon) navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
|
||||
else fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true }).catch(function(){});
|
||||
else fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true }).catch(function(error){ console.warn("[wrnexus:observability] web-vitals delivery failed", error); });
|
||||
}
|
||||
function flush() {
|
||||
if (flushed) return;
|
||||
@@ -31,9 +31,13 @@ export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
|
||||
if (inp) send("INP", inp);
|
||||
if (cls) send("CLS", cls);
|
||||
}
|
||||
try { new PerformanceObserver(function(list){ var entries=list.getEntries(); var last=entries[entries.length-1]; if(last) lcp = Math.max(lcp, last.startTime); }).observe({type:"largest-contentful-paint",buffered:true}); } catch(_) {}
|
||||
try { new PerformanceObserver(function(list){ list.getEntries().forEach(function(e){ if(!e.hadRecentInput) cls += e.value; }); }).observe({type:"layout-shift",buffered:true}); } catch(_) {}
|
||||
try { new PerformanceObserver(function(list){ list.getEntries().forEach(function(e){ inp=Math.max(inp,e.duration||0); }); }).observe({type:"event",durationThreshold:40,buffered:true}); } catch(_) {}
|
||||
function observe(type, callback, options) {
|
||||
try { new PerformanceObserver(callback).observe(options); }
|
||||
catch(error) { console.warn("[wrnexus:observability] PerformanceObserver unavailable for " + type, error); }
|
||||
}
|
||||
observe("largest-contentful-paint", function(list){ var entries=list.getEntries(); var last=entries[entries.length-1]; if(last) lcp = Math.max(lcp, last.startTime); }, {type:"largest-contentful-paint",buffered:true});
|
||||
observe("layout-shift", function(list){ list.getEntries().forEach(function(e){ if(!e.hadRecentInput) cls += e.value; }); }, {type:"layout-shift",buffered:true});
|
||||
observe("event", function(list){ list.getEntries().forEach(function(e){ inp=Math.max(inp,e.duration||0); }); }, {type:"event",durationThreshold:40,buffered:true});
|
||||
addEventListener("visibilitychange", function(){ if(document.visibilityState === "hidden") flush(); });
|
||||
addEventListener("pagehide", flush, { once: true });
|
||||
})();`;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export type ObservabilityDiagnostic = (message: string, error: unknown) => void;
|
||||
|
||||
/** Report exporter failures without feeding them back through the exporter. */
|
||||
export function reportObservabilityFailure(
|
||||
message: string,
|
||||
error: unknown,
|
||||
diagnostic?: ObservabilityDiagnostic,
|
||||
): void {
|
||||
if (diagnostic) {
|
||||
diagnostic(message, error);
|
||||
return;
|
||||
}
|
||||
console.warn(`[wrnexus:observability] ${message}`, error);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { MetricPoint } from "./metrics.ts";
|
||||
import type { LogRecord } from "./logging.ts";
|
||||
import type { MetricExporter } from "./server.ts";
|
||||
import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts";
|
||||
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";
|
||||
|
||||
export type FrameworkSpanKind =
|
||||
"database" | "cache" | "queue" | "realtime" | "server-action" | "application";
|
||||
@@ -26,6 +27,7 @@ export function createOperationTracer(
|
||||
context?: () => Partial<TraceContext>;
|
||||
now?: () => number;
|
||||
onExportError?: (error: unknown) => void;
|
||||
diagnostic?: ObservabilityDiagnostic;
|
||||
} = {},
|
||||
): OperationTracer {
|
||||
const now = options.now ?? Date.now;
|
||||
@@ -66,7 +68,9 @@ export function createOperationTracer(
|
||||
try {
|
||||
await options.exporter?.export([record]);
|
||||
} catch (error) {
|
||||
options.onExportError?.(error);
|
||||
if (options.onExportError) options.onExportError(error);
|
||||
else
|
||||
reportObservabilityFailure("operation span export failed", error, options.diagnostic);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createTracer, type Context, type Middleware } from "@wrnexus/core";
|
||||
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";
|
||||
|
||||
export interface TraceContext {
|
||||
version: "00";
|
||||
@@ -63,6 +64,7 @@ export interface TraceMiddlewareOptions {
|
||||
routeName?: (ctx: Context) => string;
|
||||
serverTiming?: boolean;
|
||||
onExportError?: (error: unknown, span: SpanRecord) => void | Promise<void>;
|
||||
diagnostic?: ObservabilityDiagnostic;
|
||||
}
|
||||
|
||||
export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middleware {
|
||||
@@ -131,7 +133,8 @@ export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middlewar
|
||||
await options.onSpan?.(span);
|
||||
await options.exporter?.export([span]);
|
||||
} catch (error) {
|
||||
await options.onExportError?.(error, span);
|
||||
if (options.onExportError) await options.onExportError(error, span);
|
||||
else reportObservabilityFailure("trace export failed", error, options.diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user