first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
# @wrnexus/tracking
> Error tracking for WrNexus apps: capture exceptions manually or via middleware and fan them out to pluggable sinks.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Overview
`@wrnexus/tracking` is a small, server-side error-capture layer. You create a
tracker with one or more **sinks**, then feed it errors — either manually with
`tracker.capture(err, context)` or automatically by mounting `tracker.middleware()`
in your request pipeline. A `consoleSink` is included; forwarding to Sentry,
Datadog, or any other backend is just a matter of writing a tiny sink. Reach for
it when you want a single, sink-agnostic place to route application errors. Sinks
run best-effort — a throwing sink never breaks the request.
## Installation
```bash
bun add @wrnexus/tracking
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### `createTracker(options?): Tracker`
Creates a tracker. `TrackerOptions`:
| Option | Type | Description |
| ------------ | ------------------------------------------- | --------------------------------------------------------------------------- |
| `sinks` | `ErrorSink[]` | Initial sinks to fan events out to. Defaults to `[]`. |
| `now` | `() => number` | Clock used for `event.timestamp` (epoch ms). Defaults to `Date.now`. |
| `beforeSend` | `(event: ErrorEvent) => ErrorEvent \| null` | Scrub/enrich an event before it reaches any sink. Return `null` to drop it. |
The returned `Tracker`:
| Member | Signature | Description |
| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `capture` | `(error: unknown, context?: Record<string, unknown>) => Promise<void>` | Normalizes any thrown value into an `Error`, builds an `ErrorEvent`, runs `beforeSend`, then dispatches to all sinks. Non-`Error` values are wrapped in an `Error` named `NonError`. |
| `addSink` | `(sink: ErrorSink) => void` | Registers an additional sink at runtime. |
| `middleware` | `() => Middleware` | Returns a WrNexus `Middleware` that captures any error thrown downstream, then re-throws it so the framework's error handler still produces the response. |
The middleware attaches this context to captured events:
```ts
{ method: ctx.req.method, path: ctx.url.pathname, requestId: ctx.locals.requestId }
```
### `consoleSink: ErrorSink`
A built-in sink that logs a compact one-line message via `console.error`, e.g.
`[error] TypeError: cannot read x {"userId":42}`.
### Types
```ts
interface ErrorEvent {
error: Error;
context: Record<string, unknown>; // request info, user id, tags…
timestamp: number; // epoch ms
}
interface ErrorSink {
name?: string;
capture(event: ErrorEvent): void | Promise<void>;
}
```
## Usage
Manual capture:
```ts
import { createTracker, consoleSink } from "@wrnexus/tracking";
const tracker = createTracker({ sinks: [consoleSink] });
try {
await doWork();
} catch (err) {
await tracker.capture(err, { userId: 42, op: "doWork" });
throw err;
}
```
As request middleware:
```ts
import { createTracker, consoleSink } from "@wrnexus/tracking";
const tracker = createTracker({ sinks: [consoleSink] });
app.use(tracker.middleware()); // captures + re-throws downstream errors
```
A custom sink with `beforeSend` scrubbing:
```ts
import { createTracker, type ErrorSink } from "@wrnexus/tracking";
const sentrySink: ErrorSink = {
name: "sentry",
async capture(event) {
await Sentry.captureException(event.error, { extra: event.context });
},
};
const tracker = createTracker({
sinks: [sentrySink],
beforeSend(event) {
delete event.context.password; // scrub secrets
return event; // return null to drop the event entirely
},
});
tracker.addSink(anotherSink); // add more sinks later
```
## Requirements / Notes
- Runs on **Bun** only (Node is not supported).
- Peer package: [`@wrnexus/core`](../core) — the `Context` and `Middleware` types
used by `tracker.middleware()` come from there.
- Sink dispatch is fire-and-forget-safe: all sinks run via `Promise.all`, and a
sink that throws is swallowed so it can never break the app.
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@wrnexus/tracking",
"version": "0.2.12",
"private": true,
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* @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
}
};
},
};
}
+61
View File
@@ -0,0 +1,61 @@
import { test, expect } from "bun:test";
import { createContext } from "@wrnexus/core";
import { createTracker, type ErrorEvent, type ErrorSink } from "../src/index.ts";
function collectingSink(): ErrorSink & { events: ErrorEvent[] } {
const events: ErrorEvent[] = [];
return { name: "collect", events, capture: (e) => void events.push(e) };
}
test("capture fans out to sinks with context + timestamp", async () => {
const sink = collectingSink();
const tracker = createTracker({ sinks: [sink], now: () => 123 });
await tracker.capture(new Error("boom"), { userId: "u1" });
expect(sink.events.length).toBe(1);
expect(sink.events[0]!.error.message).toBe("boom");
expect(sink.events[0]!.context.userId).toBe("u1");
expect(sink.events[0]!.timestamp).toBe(123);
});
test("non-Error values are wrapped", async () => {
const sink = collectingSink();
const tracker = createTracker({ sinks: [sink] });
await tracker.capture("just a string");
expect(sink.events[0]!.error.message).toBe("just a string");
});
test("beforeSend can scrub or drop events", async () => {
const sink = collectingSink();
const tracker = createTracker({
sinks: [sink],
beforeSend: (e) => (e.context.secret ? null : e), // drop events tagged secret
});
await tracker.capture(new Error("a"), { secret: true });
await tracker.capture(new Error("b"));
expect(sink.events.map((e) => e.error.message)).toEqual(["b"]);
});
test("middleware captures a thrown request error and re-throws it", async () => {
const sink = collectingSink();
const tracker = createTracker({ sinks: [sink] });
const url = new URL("http://x/api/boom");
const ctx = createContext(new Request(url, { method: "POST" }), url);
await expect(
tracker.middleware()(ctx, () => {
throw new Error("downstream");
}),
).rejects.toThrow("downstream");
expect(sink.events[0]!.context).toMatchObject({ method: "POST", path: "/api/boom" });
});
test("a failing sink never breaks capture", async () => {
const bad: ErrorSink = {
capture: () => {
throw new Error("sink down");
},
};
const good = collectingSink();
const tracker = createTracker({ sinks: [bad, good] });
await tracker.capture(new Error("x")); // must not throw
expect(good.events.length).toBe(1);
});