@autter/runtime-node 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,81 @@ application context to logs and completed operations. See the full
5
5
  [operation logging guide](../../docs/OPERATION-LOGGING.md) for initialization,
6
6
  steps, outcomes, redaction, export lifecycle, and release requirements.
7
7
 
8
+ # Requests and coded errors (1.5.0)
9
+
10
+ Requires ingester 1.5.0. One summary per request (always kept), request ids
11
+ echoed to clients, one issue per error code:
12
+
13
+ ```ts
14
+ import {
15
+ autterRequests, autterErrorResponse, defineRuntimeErrors, runtimeContext,
16
+ } from "@autter/runtime-node";
17
+
18
+ export const billingErrors = defineRuntimeErrors("billing", {
19
+ declined: { status: 402, message: "Payment declined", expected: true,
20
+ why: "The card issuer rejected the charge", fix: "Ask for another card" },
21
+ limit: ({ plan }: { plan: string }) => ({ status: 429, message: `Plan ${plan} limit reached` }),
22
+ });
23
+
24
+ app.use(autterRequests({ ignore: ["/healthz", "/metrics"] }));
25
+ app.post("/checkout", async (req, res) => {
26
+ runtimeContext.set({ cart: { items: req.body.items.length } });
27
+ runtimeContext.info("Applied coupon", { coupon: "SPRING" }); // folded into the summary
28
+ if (overLimit) throw billingErrors.limit({ plan: "free" }); // Express 5 (next(err) on 4)
29
+ res.json({ ok: true, requestId: runtimeContext.requestId });
30
+ });
31
+ app.use(autterErrorResponse()); // { error: { message, code, why?, fix?, link?, requestId } }
32
+ ```
33
+
34
+ Also: `autterFastify`, `withRuntimeRequest` (fetch-style handlers),
35
+ `runtimeContext.fork` / `runInBackground` / `carrier()` for background work and
36
+ queues, the per-operation AI usage rollup, `waitUntil`, `initAutterLogging`
37
+ (no NodeSDK — for apps with their own OTel), enrichers (`enrichUserAgent`,
38
+ `enrichRequestSize`, `enrichEdgeGeo`, `enrichDeployment`), sinks (`otlpSink`,
39
+ `consoleSink`, `fileSink` → `.autter/runtime/*.jsonl` in development) and test
40
+ helpers:
41
+
42
+ ```ts
43
+ import { captureRuntime, expectOperation } from "@autter/runtime-node/testing";
44
+ const runtime = captureRuntime();
45
+ // … exercise the app …
46
+ expectOperation(runtime, "POST /checkout").toHaveOutcome("degraded").toHaveErrorCode("billing.declined");
47
+ ```
48
+
49
+ Full guide: [Requests, coded errors and background work](../../docs/REQUESTS-AND-ERRORS.md).
50
+
51
+ # Breadcrumbs (1.6.0)
52
+
53
+ The steps before an error, scoped to the current request:
54
+
55
+ ```ts
56
+ import { addBreadcrumb, runtimeLogger } from "@autter/runtime-node";
57
+
58
+ app.post("/checkout", async (req, res) => {
59
+ addBreadcrumb({ message: "Loaded cart", data: { items: 3 } }); // type "custom"
60
+ runtimeLogger.info("Charging card", { provider: "stripe" }); // recorded as a "log" breadcrumb
61
+ await charge(); // a throw here carries both
62
+ });
63
+ ```
64
+
65
+ - Scope: inside `autterRequests()` / `withRuntimeRequest()` /
66
+ `withRuntimeOperation()` the trail belongs to the request and is shared with
67
+ its child operations and forks (AsyncLocalStorage). Inside a traced request
68
+ without the middleware it belongs to the trace; outside both, to the process
69
+ (so a crash outside any request carries what happened before it).
70
+ - The last 30 are kept. `captureException`, `captureMessage` and errors thrown
71
+ out of operations attach them to the exception span event as
72
+ `autter.breadcrumbs` (a JSON string, at most 16 KB, or less when
73
+ `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` is lower).
74
+ - Every `runtimeLogger` / `runtimeContext.debug|info|warn|error` entry is
75
+ recorded as a `log` breadcrumb with its level and up to 10 primitive
76
+ attributes.
77
+ - Messages (≤ 500 characters) and data (≤ 10 keys of strings, numbers,
78
+ booleans or null) are scrubbed with your `redact` settings.
79
+ - The browser relay forwards browser breadcrumbs, re-validated and re-scrubbed.
80
+ - Needs ingester 1.6.0. Logger-only mode (`initAutterLogging`) and
81
+ `@autter/runtime-edge` don't attach them yet.
82
+
8
83
  # @autter/runtime-node
9
84
 
10
85
  The server tracker exports per-instance RSS, heap, memory limit, and GC
@@ -70,6 +145,12 @@ const autter = initAutterServer({
70
145
  // handled errors — always recorded, never sampled out:
71
146
  autter.captureException(err, { "order.id": "…" });
72
147
 
148
+ // fingerprint override — every error carrying the same fingerprint is one
149
+ // issue, whatever its message or stack (a string or up to 10 strings, each
150
+ // ≤200 chars; sent as `autter.fingerprint` on the exception event; needs
151
+ // ingester 1.6.0+, older ingesters ignore it):
152
+ autter.captureException(err, { fingerprint: ["checkout", "card-timeout"] });
153
+
73
154
  // warnings/info without an exception — same grouping+aggregation as
74
155
  // errors, just a lower severity ("fatal" | "error" | "warning" | "info"):
75
156
  autter.captureMessage("Legacy /orders lookup used", "warning");
@@ -99,7 +180,7 @@ Defaults (cheap by construction):
99
180
  | Traces containing an error | 100% (tail retention, `retainTracesOnError`) |
100
181
  | `withProcessSpan` spans | 100% (same always-on tracer) |
101
182
  | LLM/GenAI call spans | 100% (`llmTracing`, on by default) |
102
- | Healthy traces | 1% head sampling (`traceSampleRate`) |
183
+ | Healthy traces | 1% head sampling (`traceSampleRate`; also for requests whose incoming `traceparent` is unsampled, such as browser fetches from `@autter/runtime-browser`) |
103
184
  | Request metrics | exported every 60 s |
104
185
  | Logs | not collected |
105
186
  | PII in custom attributes | redacted before export (`redactAttributes`) |
@@ -124,6 +205,92 @@ import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
124
205
  initAutterServer({ ..., instrumentations: [new ExpressInstrumentation()] });
125
206
  ```
126
207
 
208
+ ### Server errors keep their stacks
209
+
210
+ When a handler throws, the framework answers 500 — and on its own that 500
211
+ is all the tracker sees: a stackless `HttpServerError` grouped by route. Add
212
+ your framework's hook and the thrown error is recorded, with its stack, on
213
+ the request span; the framework then carries on exactly as before.
214
+
215
+ ```ts
216
+ import { autterErrorHandler, autterFastifyErrors, autterKoa, captureRequestError } from "@autter/runtime-node";
217
+
218
+ // Express / Connect: after your routes, before your own error handlers.
219
+ // Records the error, then calls next(err) unchanged.
220
+ app.use(autterErrorHandler());
221
+
222
+ // Fastify 4/5: an onError hook (Fastify's error handler still answers).
223
+ // autterFastify (request summaries) already includes it.
224
+ app.register(autterFastifyErrors); // or autterFastifyErrors(app)
225
+
226
+ // Koa 2/3: mount first; records and rethrows, so app.on("error") still fires.
227
+ app.use(autterKoa());
228
+
229
+ // Anything else (NestJS exception filters, hapi onPreResponse, …):
230
+ captureRequestError(err, { "app.handler": "checkout" });
231
+ ```
232
+
233
+ All three report errors with status ≥ 500 (or none), RuntimeErrors and coded
234
+ errors — `ctx.throw(404)` and validation failures stay out of your issues.
235
+ Pass `{ capture: true }` to report every error, or a predicate. An error is
236
+ reported once, even if your handler already called `captureException`, and it
237
+ lands on the request's HTTP server span even when an instrumentation's child
238
+ span is active (`captureRequestError` still adds its attributes to that span
239
+ when the error was already reported). Errors from another JavaScript realm
240
+ (a `vm` context, Next's edge sandbox under `next start`) keep their own type,
241
+ message and stack. `autterErrorResponse()` reports the same way and also
242
+ answers the request.
243
+
244
+ ### Unhandled promise rejections
245
+
246
+ Recorded as errors (severity `error`, `autter.unhandled_rejection: true`, not
247
+ fatal) — without changing whether your process crashes. Any
248
+ `unhandledRejection` listener switches off Node's default crash, so the SDK
249
+ does what Node would have done when its listener is the only one:
250
+
251
+ | `--unhandled-rejections` | Your own listener? | What happens | Recorded as |
252
+ | --- | --- | --- | --- |
253
+ | `throw` (default) | no | rethrown on the next tick: exit code 1, as before | fatal crash (`autter.unhandled`) |
254
+ | `throw` (default) | yes | your listener decides | error |
255
+ | `strict` | either | Node crashes first, as before | fatal crash |
256
+ | `warn`, `none` | either | no crash, as before | error |
257
+ | `warn-with-error-code` | no | Node's warning and exit code 1, as before | error |
258
+
259
+ The one visible difference: after the rethrow, `uncaughtException` listeners
260
+ receive origin `"uncaughtException"` rather than `"unhandledRejection"`.
261
+ `captureUnhandledRejections: false` (or `captureGlobalErrors: false`) restores
262
+ the 1.5.0 behaviour, where a rejection is seen only when it crashes the
263
+ process.
264
+
265
+ ### Version compatibility: checked for you
266
+
267
+ Some features need a minimum ingester version: operation logging needs
268
+ 1.4.0, memory metrics 1.3.3, endpoint latency 1.3.1. After
269
+ `initAutterServer`, the SDK checks the ingester once in the background
270
+ (`GET /v1/compat`, unref'd, 3 s timeout). For each feature in use that the
271
+ ingester can't store, it prints one warning:
272
+
273
+ ```text
274
+ [autter-runtime] Operation logging needs ingester >= 1.4.0; yours is 1.3.4. Upgrade the ingester: docker pull ghcr.io/autter-dev/otlp-ingester:latest and restart it (ClickHouse migrations run at boot). See …/docs/COMPATIBILITY.md
275
+ ```
276
+
277
+ The check never throws, never delays startup or exit, and is silent when
278
+ versions match or are unknown. `debug: true` logs the result. Turn it off
279
+ with `compatCheck: false` or `AUTTER_COMPAT_CHECK=0`. The browser relay does
280
+ the same for browser features such as CSP violation capture. The SDK reports
281
+ itself through the OTLP resource attributes `telemetry.distro.name` and
282
+ `telemetry.distro.version`, so the Autter dashboard shows the SDK version
283
+ each service runs.
284
+
285
+ For a full report (for example in CI or after a deploy):
286
+
287
+ ```bash
288
+ npx @autter/runtime-node doctor --endpoint https://ingest.example.com [--key "$AUTTER_RUNTIME_KEY"] [--json]
289
+ ```
290
+
291
+ Exit codes: 0 compatible, 1 mismatch or rejected key, 2 ingester unreachable.
292
+ See [docs/COMPATIBILITY.md](../../docs/COMPATIBILITY.md).
293
+
127
294
  ### Lifecycle: never lose telemetry to a forgotten shutdown()
128
295
 
129
296
  Telemetry is batched (errors every ~2 s, healthy traces every ~5 s, metrics
@@ -155,23 +322,39 @@ default 1% sampling the span contribution is negligible; if you set
155
322
  Tail-retained error traces don't distort this: their spans carry
156
323
  `autter.tail_retained` and the ingester keeps them out of span-fed rollups.
157
324
 
158
- ### Privacy: custom attributes are redacted before they leave the process
159
-
160
- The browser relay whitelist-sanitises everything a client posts — but the
161
- server tracker accepts free-form attributes from *your* code, where a stray
162
- `captureException(err, { "user.email": … })` would otherwise go out
163
- verbatim. Custom attributes are therefore scrubbed at capture time by
164
- default:
165
-
166
- - values that look like emails, JWTs, `sk-…`/`ghp_…`/AWS/Slack tokens,
167
- `Bearer …` headers, or `scheme://user:pass@host` URLs are masked;
325
+ ### Privacy: secrets and PII are scrubbed before they leave the process
326
+
327
+ Error context is where secrets leak: `connect ECONNREFUSED
328
+ postgres://admin:hunter2@db…` in an exception message, a provider error
329
+ echoing an API key, a `?token=` in a request URL, a stray
330
+ `captureException(err, { "user.email": … })`. Everything the SDK exports is
331
+ therefore scrubbed by default — custom attributes, exception messages and
332
+ stack traces, span status messages, LLM call attributes and errors,
333
+ structured logs, and the browser events the relay forwards. A final pass at
334
+ export time scrubs every span regardless of who created it (HTTP
335
+ instrumentation URLs, third-party instrumentations, `recordException` in
336
+ your own code).
337
+
338
+ - secrets inside any string are masked in place, keeping the rest of the
339
+ message/stack readable: JWTs, `Bearer`/`Basic` credentials,
340
+ `Authorization:`/`Cookie:`/`Set-Cookie:` header text,
341
+ `scheme://user:pass@host` connection strings (postgres, mysql,
342
+ mongodb+srv, redis, …), vendor keys (`sk-…`, `sk_live_…`, `ghp_…`,
343
+ `github_pat_…`, `xox?-…`, `AIza…`, `AKIA…`, …), PEM private keys,
344
+ `password=`/`?token=`/`?api_key=`-style assignments, emails, and
345
+ Luhn-valid card numbers;
168
346
  - attributes whose **key** looks sensitive (`password`, `token`, `secret`,
169
- `api_key`, `authorization`, `cookie`, `ssn`, `card_number`, …) are masked
170
- wholesale;
171
- - non-sensitive keys and primitives pass through untouched, so grouping and
172
- dashboards keep working.
347
+ `api_key`, `authorization`, `cookie`, `session`, `ssn`, `card_number`, …)
348
+ are masked wholesale, at any nesting depth;
349
+ - non-sensitive keys, numbers (including LLM token counts) and booleans pass
350
+ through untouched, so grouping, dashboards and cost tracking keep working.
351
+
352
+ The same patterns run in the browser SDK, the relay, the Python adapter,
353
+ and again in the ingester before storage (shared test vectors in
354
+ `test-vectors/redaction.json`).
173
355
 
174
- Disable or extend it per service:
356
+ Extend it (applies to messages, stacks and URLs too) or disable it per
357
+ service:
175
358
 
176
359
  ```ts
177
360
  initAutterServer({
@@ -193,7 +376,9 @@ const safe = makeSafeCapture();
193
376
  safe.captureException(err, { "user.email": email }); // masked before export
194
377
  ```
195
378
 
196
- The raw primitive is exported too (`redactAttributes(attrs, options)`).
379
+ The raw primitives are exported too (`redactAttributes(attrs, options)`,
380
+ `redactText(text, options)`). The browser relay takes the same options as
381
+ `createBrowserRelayHandler({ ..., redact })`.
197
382
  This closes the server-side gap to match the browser relay's payload
198
383
  whitelist; it is best-effort scrubbing of obvious PII shapes, not a DLP
199
384
  engine — keep secrets out of attributes in the first place.