/** * @wrnexus/tracking — error tracking with pluggable sinks. Capture exceptions * manually or via middleware, and fan them out to any sink (console by default; * write a small sink to forward to Sentry/Datadog/etc.). * * const tracker = createTracker({ sinks: [consoleSink] }); * app-middleware: tracker.middleware() // captures + re-throws request errors * tracker.capture(err, { userId }); // manual */ import type { Context, Middleware } from "@wrnexus/core"; export interface ErrorEvent { error: Error; /** Arbitrary structured context (request info, user id, tags…). */ context: Record; /** Epoch ms. */ timestamp: number; } export interface ErrorSink { name?: string; capture(event: ErrorEvent): void | Promise; } export interface Tracker { capture(error: unknown, context?: Record): Promise; addSink(sink: ErrorSink): void; /** Middleware that captures errors thrown downstream, then re-throws them. */ middleware(): Middleware; } export interface TrackerOptions { sinks?: ErrorSink[]; now?: () => number; /** Scrub/enrich an event before it hits sinks (return null to drop it). */ beforeSend?: (event: ErrorEvent) => ErrorEvent | null; } /** A sink that logs a compact one-line error to the console. */ export const consoleSink: ErrorSink = { name: "console", capture(event) { const ctx = Object.keys(event.context).length ? ` ${JSON.stringify(event.context)}` : ""; console.error(`[error] ${event.error.name}: ${event.error.message}${ctx}`); }, }; function toError(value: unknown): Error { if (value instanceof Error) return value; const err = new Error(typeof value === "string" ? value : JSON.stringify(value)); err.name = "NonError"; return err; } export function createTracker(options: TrackerOptions = {}): Tracker { const sinks = [...(options.sinks ?? [])]; const now = options.now ?? Date.now; const capture: Tracker["capture"] = async (error, context = {}) => { let event: ErrorEvent | null = { error: toError(error), context, timestamp: now() }; if (options.beforeSend) event = options.beforeSend(event); if (!event) return; await Promise.all( sinks.map(async (sink) => { try { await sink.capture(event as ErrorEvent); } catch { /* a failing sink must never break the app */ } }), ); }; return { capture, addSink(sink) { sinks.push(sink); }, middleware(): Middleware { return async (ctx: Context, next) => { try { return await next(); } catch (error) { await capture(error, { method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId, }); throw error; // let the framework's error handler produce the response } }; }, }; } export { createTelemetryPipeline, telemetryConsoleSink } from "./pipeline.ts"; export type { TelemetryKind, TelemetryEnvelope, TelemetrySink, TelemetryPipeline, TelemetryPipelineOptions, } from "./pipeline.ts";