# Winston

The `@newinstance/bugwatch/winston` transport forwards every log record Winston writes to BugWatch, next to your existing transports. It is a `winston-transport` implementation and does not replace your console or file output.

## Install

```sh
npm install @newinstance/bugwatch
```

That is the whole install. `winston` (>= 3) is an optional peer dependency you already have (it brings `winston-transport` with it). There is no separate plugin package.

Get a project key from the BugWatch dashboard at [www.newinstance.cloud](https://www.newinstance.cloud),
under BugWatch, your project, Settings, DSN keys. Use the `sk_test_...` key in test and `sk_live_...`
in production.

## Adding the transport

```ts
import winston from "winston";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchWinstonTransport } from "@newinstance/bugwatch/winston";

const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });

const logger = winston.createLogger({
	level: "info",
	transports: [
		new winston.transports.Console(),
		createBugWatchWinstonTransport(client),
	],
});

logger.error("payment gateway timeout", { gateway: "stripe" });
```

The second argument is a standard `TransportStreamOptions`, so you can forward only the noisy-free
tail of your logs while the console keeps everything:

```ts
createBugWatchWinstonTransport(client, { level: "warn" });
```

The transport is failure isolated: anything that goes wrong inside it is swallowed, so a BugWatch
problem can never throw into your logging path.

## Level mapping

Winston level names are translated to BugWatch numeric severities (the Pino-compatible 10 to 60
scale) before the event is sent.

| Winston level      | BugWatch level |
| ------------------ | -------------- |
| `fatal`            | 60             |
| `error`            | 50             |
| `warn`             | 40             |
| `info`, `http`     | 30             |
| `verbose`, `debug` | 20             |
| `silly`, `trace`   | 10             |

Any level name not in that table (a custom level from `winston.addColors` / custom `levels`) falls
back to `info` (30). If you use custom level names and care about severity in the dashboard, name
them after one of the rows above.

## Tags are scalar only

Every property on the Winston `info` object other than `level`, `message`, and `stack` becomes a
BugWatch tag, but only if its value is a `string`, `number`, `boolean`, or `bigint`. Each value is
stringified. Objects, arrays, `null`, and `undefined` are dropped so events stay flat.

```ts
logger.warn("slow query", {
	table: "orders", // kept, tag table="orders"
	duration_ms: 1200, // kept, tag duration_ms="1200"
	cached: false, // kept, tag cached="false"
	query: { where: { id: 7 } }, // DROPPED, object
});
```

Workaround: flatten or stringify before logging.

```ts
logger.warn("slow query", {
	table: "orders",
	duration_ms: 1200,
	query: JSON.stringify({ where: { id: 7 } }),
});
```

If the message itself is not a string, it is coerced with `String(...)`.

## Errors and stacktraces through Winston (read this)

Be aware of what the transport does not do. It calls `captureLog` with a level, a message, and
tags. It never passes an `error` value, and it explicitly skips the `stack` key on the `info`
object.

Consequences:

- `logger.error(new Error("db down"))` arrives as a level 50 log whose message is the error
  message. No stacktrace, no exception type, no grouping as an exception.
- `winston.format.errors({ stack: true })` does not help. It puts the stack on `info.stack`, which
  is exactly the key the transport skips.
- `logger.error("db down", { err })` loses `err` entirely, because an `Error` is an object and the
  tag filter keeps scalars only.
- Cause chains (`new Error(msg, { cause })`) are not reconstructed, because no error object is
  forwarded.

Recommended pattern: keep Winston for logs, and capture exceptions directly.

```ts
try {
	await processPayment(order);
} catch (err) {
	logger.error("payment failed", { orderId: order.id }); // human-readable log line
	client.captureException(err, { tags: { orderId: order.id } }); // full stacktrace + grouping
}
```

`captureException` normalizes the `Error`, keeps the stacktrace, follows `cause` chains, and groups
occurrences in the dashboard. Reach for it for anything you want to triage as an exception.

## Request scope enrichment and trace correlation

Forwarded logs are captured through the same pipeline as `captureException`, so they inherit the
active scope. When a framework adapter (express, koa, fastify, hapi, nest, next) opens its
per-request `AsyncLocalStorage` scope, a `logger.info(...)` inside that request automatically picks
up the request's `method`, `url`, `route`, `requestId`, its user (from the adapter's `getUser`
option or `setRequestUser`), any `setRequestTag` / `setRequestContext` values, and the request's
trace and span IDs. Trace IDs come from an inbound `traceparent` header when present, otherwise a
fresh trace ID is generated per request, so logs and exceptions from one request correlate. If you
configured `traceContextProvider` (for example the OpenTelemetry bridge), it is consulted per event
too.

Concurrency is safe: each request has its own storage cell, so two overlapping requests never see
each other's user or tags.

Outside a web framework (workers, CLIs, queue consumers), wire it up yourself:

```ts
import { installAsyncScope, runWithContext } from "@newinstance/bugwatch/node";

installAsyncScope(client);

await runWithContext({ requestId: job.id, route: "job:reindex" }, async () => {
	logger.info("job started"); // arrives tagged with requestId and route
	await reindex(job);
});
```

Request scope values win over anything set globally with `client.setTag` / `client.setUser`.

## Complete example

```ts
// src/server.ts
import express, { type Request, type Response, type NextFunction } from "express";
import winston from "winston";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchWinstonTransport } from "@newinstance/bugwatch/winston";
import {
	bugWatchExpressRequestHandler,
	bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";
import { setRequestTag } from "@newinstance/bugwatch/node";

if (!process.env.BUGWATCH_KEY) throw new Error("BUGWATCH_KEY is not set");

const client = createClient({
	projectKey: process.env.BUGWATCH_KEY,
	environment: process.env.NODE_ENV ?? "development",
	release: process.env.GIT_SHA,
});

const logger = winston.createLogger({
	level: "info",
	transports: [
		new winston.transports.Console({ format: winston.format.simple() }),
		createBugWatchWinstonTransport(client, { level: "info" }),
	],
});

const app = express();
app.use(express.json());

// First: opens the per-request scope that enriches every forwarded log.
app.use(bugWatchExpressRequestHandler(client, { getUser: (req) => (req as any).user }));

app.post("/orders", async (req: Request, res: Response, next: NextFunction) => {
	setRequestTag("plan", (req.headers["x-plan"] as string) ?? "free");
	logger.info("creating order", { itemCount: req.body.items?.length ?? 0 });
	try {
		const order = await createOrder(req.body);
		logger.info("order created", { orderId: order.id, total_cents: order.totalCents });
		res.json(order);
	} catch (err) {
		// Log line for humans, plus a real exception with the stacktrace.
		logger.error("order creation failed", { step: "createOrder" });
		client.captureException(err, { tags: { step: "createOrder" } });
		next(err);
	}
});

// Last: captures anything thrown further up the stack.
app.use(bugWatchExpressErrorHandler(client));

const server = app.listen(3000, () => logger.info("listening", { port: 3000 }));

process.on("SIGTERM", async () => {
	server.close();
	await client.close(); // flushes pending events before exit
});

async function createOrder(body: unknown): Promise<{ id: string; totalCents: number }> {
	throw new Error("payment gateway timeout");
}
```

## Troubleshooting

**Logs never appear in the dashboard.** Events are batched and flushed on an interval. In
short-lived processes (scripts, serverless handlers, containers being torn down) call
`await client.flush()` or `await client.close()` before exit. Turn on `debug: true` in
`createClient` and watch for `[bugwatch]` delivery-failure diagnostics (debug mode prints failures, not successes).

**Some log fields are missing.** Only scalar values become tags. See the scalar-only section above
and stringify nested data.

**No stacktrace on an error log.** Expected. The Winston transport does not forward errors or
stacks. Use `client.captureException(err)`.

**Everything shows up as `info`.** Your logger uses custom level names that are not in the mapping
table, so they fall back to 30. Rename them, or capture severity-critical events with
`client.captureLog({ level: 50, message })`.

**Nothing at all is forwarded, but the console transport works.** Check the transport's own `level`
option and the logger's `level`. Winston filters per transport, and the more restrictive of the two
wins.

**`401 Invalid or inactive API key` in debug output.** The key is revoked or from the wrong
environment. Mint a fresh DSN key under BugWatch, your project, Settings, DSN keys, and confirm
`sk_live_...` in production.
