@watchmecode/retrace-sdk
v0.4.0
Published
Capture backend events (HTTP, DB, jobs, external & LLM calls) and replay failures on a visual timeline. Fastify plugin + manual client, with automatic trace context via AsyncLocalStorage.
Maintainers
Readme
@watchmecode/retrace-sdk
Capture backend events and replay failures on a visual timeline.
The SDK sends structured events (HTTP requests, DB queries, job/queue steps,
external & LLM calls, errors) to a ReTrace API. Events that share a
traceId are grouped server-side into a single incident — one failed
request/flow = one timeline you can scrub through.
Install
npm install @watchmecode/retrace-sdkQuick start (Fastify)
import Fastify from 'fastify'
import { fastifyRetrace } from '@watchmecode/retrace-sdk'
const app = Fastify()
await app.register(
fastifyRetrace({
apiUrl: 'https://your-retrace-api',
apiKey: process.env.RETRACE_API_KEY, // rtrace_xxx — ties events to your project
captureBody: true,
errorThreshold: 400, // open an incident on any 4xx/5xx
})
)Every request is now captured automatically: an HTTP_REQUEST event named
"<METHOD> <route> → <status>", plus a full stack trace for unhandled (5xx)
errors. Handled client errors (4xx) are recorded once, via the response — no
duplicate noise.
Quick start (Express)
The same core, exposed as Express middleware via a subpath import:
import express from 'express'
import { expressRetrace } from '@watchmecode/retrace-sdk/express'
const app = express()
app.use(express.json())
const retrace = expressRetrace({
apiUrl: 'https://your-retrace-api',
apiKey: process.env.RETRACE_API_KEY,
captureBody: true,
})
app.use(retrace) // capture every request + bind trace context
// ...your routes...
app.use(retrace.errorHandler) // optional: capture unhandled (5xx) errors with a stackBehaviour matches the Fastify plugin (same events, redaction, and automatic
trace context below). express is an optional peer dependency — it's only
loaded through the /express subpath, so Fastify users never pull it in.
Quick start (NestJS)
A global interceptor + module, wired in one line:
import { Module } from '@nestjs/common'
import { RetraceModule } from '@watchmecode/retrace-sdk/nestjs'
@Module({
imports: [
RetraceModule.forRoot({
apiUrl: 'https://your-retrace-api',
apiKey: process.env.RETRACE_API_KEY,
captureBody: true,
}),
],
})
export class AppModule {}RetraceModule.forRoot() registers a global APP_INTERCEPTOR that captures
every request (with the same events, redaction, and trace context) and reports
HttpException statuses correctly. Inject the shared client with
@Inject(RETRACE_CLIENT) for manual captures. @nestjs/* and rxjs are
optional peers, loaded only via the /nestjs subpath.
✨ Automatic trace context (AsyncLocalStorage)
The headline feature. The Fastify plugin binds the request's traceId to its
async context, so manual captures anywhere in the call chain auto-correlate to
the same incident — you never thread a traceId through your function
signatures.
import { createClient } from '@watchmecode/retrace-sdk'
const retrace = createClient({ apiUrl, apiKey })
app.post('/checkout', async (req, reply) => {
await chargeCard() // ← captures inside here…
await reserveStock() // ← …and here…
// …all land in the SAME incident as this request, no traceId passed around.
})
async function chargeCard() {
try {
await gateway.charge()
} catch (err) {
retrace.captureException(err, { source: 'payment-gateway', label: 'charge' })
throw err
}
}Outside an HTTP request (background jobs, queue consumers) open a scope yourself:
import { runWithTrace, generateTraceId } from '@watchmecode/retrace-sdk'
await runWithTrace({ traceId: generateTraceId() }, async () => {
await processJob() // every capture inside joins one incident
})Manual capture API
// A single event. incidentId/traceId are optional — omit them and the API
// groups by the active trace context.
retrace.capture({
type: 'DB_QUERY',
source: 'inventory-service',
label: 'inventory.lock',
status: 'SUCCESS',
durationMs: 42,
})
// An error with message + stack, status ERROR. Trace context is resolved
// automatically.
retrace.captureException(err, { source: 'worker', label: 'job failed' })Options
| Option | Default | Description |
| ---------------- | ------------------------------- | -------------------------------------------- |
| apiUrl | — | Base URL of the ReTrace API (required) |
| apiKey | — | rtrace_… key; ties events to your project |
| errorThreshold | 500 | HTTP status ≥ this marks the event an error |
| captureBody | false | Capture request/response bodies |
| captureHeaders | false | Capture HTTP headers |
| maxBodySize | 8000 | Truncate captured bodies to N bytes |
| redactHeaders | authorization, cookie, … | Header names redacted from captured metadata |
| redactFields | password, token, secret, … | Body/payload field names whose values are redacted |
Redaction
Telemetry should never leak secrets. Before anything is sent:
- Sensitive headers are replaced with
[REDACTED](redactHeaders). - Sensitive body/payload fields are recursively redacted by field name —
through nested objects and arrays — for both auto-captured request bodies and
manual
capture()payloads/metadata (redactFields). Matching is case-insensitive and ignores separators, soapiKey,api_keyandAPI-KEYall match. Defaults cover passwords, tokens, secrets, keys, cookies, session ids, card numbers, CVV, SSN, and similar.
createClient({ apiUrl, redactFields: ['password', 'token', 'x-internal-id'] })
// { user: 'ann', password: 'hunter2' } → { user: 'ann', password: '[REDACTED]' }Notes
- All sends are fire-and-forget — capturing never throws and never blocks your request path.
- Sensitive headers and body fields are redacted by default; bodies are also
truncated to
maxBodySize.
