logr:
- entry.id auto-generated (UUID v4 / hex fallback) — survives backend dedup, batched-retry, cross-transport correlation.
- Transport.levels replaces minLevel — per-level { enabled, filter? } record. More flexible: enable/disable individual levels, attach per-level filters. Helper levelsAtLeast(level) for the common min-level case; allLevels() for the permissive case.
- Transport buffering: buffer, flushIntervalMs, writeBatch on the Transport interface. Logger tracks per-transport queues, flushes on size/time/flush()/detach/beforeunload. httpTransport and lokiTransport implement writeBatch for true batched HTTP delivery.
- logger.flush() to drain all buffered transports manually.
- Web Vitals integration (src/arts/logr/vitals.ts): registerWebVitals(logger, webVitalsSDK, options?) turns LCP/INP/CLS/FCP/TTFB samples into structured log entries tagged with route/rating. Zero-deps in logr — SDK is injected.
- ConsoleTransportOptions now exposes filter so vitals can be silenced on console while still flowing to Sentry/Datadog/Loki.
- Runtime name constant: `engine_logger` (via src/arts/logr/consts.ts) replaces hardcoded 'logr' string in failure reports, console prefix and synthetic entry tags.
lang:
- README expanded with a dedicated section on ts() and the LangString type: plain strings, LangRecord, LangRef; the `message: LangString` prop pattern for generic components; when to use the isLang* guards.
Tests: 185 passing (added vitals + buffer + entry.id coverage).
Docs: logr README now documents vitals (hot-zone patterns for LogQL / Sentry Discover / Datadog), buffering, the levels config, and the helpers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
parent
a6077dd357
commit
4c074e196e
@ -1,3 +1,4 @@
|
||||
export * from './consts.ts';
|
||||
export * from './engine-logger.ts';
|
||||
export * from './transports.ts';
|
||||
export * from './types.ts';
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
import type { EngineLogger } from './types.ts';
|
||||
import { LogLevel } from './types.ts';
|
||||
|
||||
/**
|
||||
* Web Vitals integration.
|
||||
*
|
||||
* Turns `web-vitals` observations into structured log entries, so every
|
||||
* transport (console, Sentry, Datadog, Loki, OTel, ...) automatically
|
||||
* sees them — useful for pinpointing slow routes, regressions and
|
||||
* device-specific problems without building a separate pipeline.
|
||||
*
|
||||
* @example
|
||||
* import * as webVitals from 'web-vitals';
|
||||
* import { registerWebVitals } from '$logr/vitals';
|
||||
*
|
||||
* registerWebVitals(logger, webVitals);
|
||||
* // Every LCP/INP/CLS/FCP/TTFB sample is now a LogEntry in the pipeline.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Standard Web Vital names. INP replaced FID as a Core Web Vital in March 2024.
|
||||
*/
|
||||
export type WebVitalName = 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
|
||||
|
||||
/**
|
||||
* Rating buckets computed by `web-vitals` based on Google's recommended
|
||||
* thresholds.
|
||||
*/
|
||||
export type WebVitalRating = 'good' | 'needs-improvement' | 'poor';
|
||||
|
||||
/**
|
||||
* Shape of the metric object emitted by `web-vitals` (subset used by logr).
|
||||
*/
|
||||
export interface WebVitalMetric {
|
||||
name: WebVitalName;
|
||||
value: number;
|
||||
rating: WebVitalRating;
|
||||
delta: number;
|
||||
id: string;
|
||||
navigationType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duck-typed subset of the `web-vitals` package. Each observer is optional
|
||||
* so the caller can opt into specific metrics without importing the rest.
|
||||
*/
|
||||
export interface WebVitalsLike {
|
||||
onLCP?: (cb: (metric: WebVitalMetric) => void, opts?: { reportAllChanges?: boolean }) => void;
|
||||
onINP?: (cb: (metric: WebVitalMetric) => void, opts?: { reportAllChanges?: boolean }) => void;
|
||||
onCLS?: (cb: (metric: WebVitalMetric) => void, opts?: { reportAllChanges?: boolean }) => void;
|
||||
onFCP?: (cb: (metric: WebVitalMetric) => void, opts?: { reportAllChanges?: boolean }) => void;
|
||||
onTTFB?: (cb: (metric: WebVitalMetric) => void, opts?: { reportAllChanges?: boolean }) => void;
|
||||
}
|
||||
|
||||
export interface VitalsOptions {
|
||||
/** Category assigned to every vitals entry. @default 'vitals' */
|
||||
category?: string;
|
||||
/**
|
||||
* Rating → LogLevel mapping. Defaults keep `good` quiet (DEBUG) so
|
||||
* production noise stays low, promote `needs-improvement` to INFO,
|
||||
* and raise `poor` to WARN so it stands out in dashboards.
|
||||
*/
|
||||
levels?: Partial<Record<WebVitalRating, LogLevel>>;
|
||||
/** Which metrics to observe. @default all available */
|
||||
metrics?: WebVitalName[];
|
||||
/** Extra tags appended to every vitals entry. */
|
||||
tags?: string[];
|
||||
/**
|
||||
* When `true`, the `web-vitals` observer calls back on every change
|
||||
* (useful for CLS / INP which accumulate). @default false
|
||||
*/
|
||||
reportAllChanges?: boolean;
|
||||
/**
|
||||
* Include `window.location.pathname` in the entry context.
|
||||
* Use this to correlate metrics with routes in dashboards.
|
||||
* @default true
|
||||
*/
|
||||
includeRoute?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_LEVELS: Record<WebVitalRating, LogLevel> = {
|
||||
good: LogLevel.DEBUG,
|
||||
'needs-improvement': LogLevel.INFO,
|
||||
poor: LogLevel.WARN
|
||||
};
|
||||
|
||||
const ALL_METRICS: WebVitalName[] = ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'];
|
||||
|
||||
/**
|
||||
* Register Web Vitals observers that emit `LogEntry` via the provided logger.
|
||||
*
|
||||
* Safe to call in SSR — no-op when `window` is undefined. Safe to call more
|
||||
* than once; every call registers its own observers (the `web-vitals`
|
||||
* internals deduplicate, but avoid double registration if possible).
|
||||
*
|
||||
* The observer API of `web-vitals` cannot be detached once registered — the
|
||||
* function returns nothing. To stop reporting, change the log level or add a
|
||||
* filter on the transport side.
|
||||
*/
|
||||
export function registerWebVitals(
|
||||
logger: EngineLogger,
|
||||
webVitals: WebVitalsLike,
|
||||
options: VitalsOptions = {}
|
||||
): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const category = options.category ?? 'vitals';
|
||||
const levels = { ...DEFAULT_LEVELS, ...(options.levels ?? {}) };
|
||||
const enabled = new Set(options.metrics ?? ALL_METRICS);
|
||||
const extraTags = options.tags ?? [];
|
||||
const includeRoute = options.includeRoute ?? true;
|
||||
const reportAllChanges = options.reportAllChanges ?? false;
|
||||
const observerOpts = reportAllChanges ? { reportAllChanges: true } : undefined;
|
||||
|
||||
const report = (metric: WebVitalMetric) => {
|
||||
const level = levels[metric.rating];
|
||||
const method = levelToMethod(level);
|
||||
|
||||
logger[method](
|
||||
category,
|
||||
`${metric.name}: ${metric.value.toFixed(2)} (${metric.rating})`,
|
||||
{
|
||||
tags: ['vitals', metric.name.toLowerCase(), metric.rating, ...extraTags],
|
||||
durationMs: metric.value,
|
||||
context: {
|
||||
metric: metric.name,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
delta: metric.delta,
|
||||
metricId: metric.id,
|
||||
navigationType: metric.navigationType,
|
||||
...(includeRoute && typeof location !== 'undefined'
|
||||
? { route: location.pathname }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (enabled.has('LCP')) webVitals.onLCP?.(report, observerOpts);
|
||||
if (enabled.has('INP')) webVitals.onINP?.(report, observerOpts);
|
||||
if (enabled.has('CLS')) webVitals.onCLS?.(report, observerOpts);
|
||||
if (enabled.has('FCP')) webVitals.onFCP?.(report, observerOpts);
|
||||
if (enabled.has('TTFB')) webVitals.onTTFB?.(report, observerOpts);
|
||||
}
|
||||
|
||||
function levelToMethod(
|
||||
level: LogLevel
|
||||
): 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' {
|
||||
switch (level) {
|
||||
case LogLevel.TRACE:
|
||||
return 'trace';
|
||||
case LogLevel.DEBUG:
|
||||
return 'debug';
|
||||
case LogLevel.INFO:
|
||||
return 'info';
|
||||
case LogLevel.WARN:
|
||||
return 'warn';
|
||||
case LogLevel.ERROR:
|
||||
return 'error';
|
||||
case LogLevel.FATAL:
|
||||
return 'fatal';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue