104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
/**
|
|
* @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<string, unknown>;
|
|
/** Epoch ms. */
|
|
timestamp: number;
|
|
}
|
|
|
|
export interface ErrorSink {
|
|
name?: string;
|
|
capture(event: ErrorEvent): void | Promise<void>;
|
|
}
|
|
|
|
export interface Tracker {
|
|
capture(error: unknown, context?: Record<string, unknown>): Promise<void>;
|
|
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";
|