@autter/runtime-node 1.0.0 → 1.2.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
@@ -61,6 +61,14 @@ autter.captureException(err, { "order.id": "…" });
61
61
  // errors, just a lower severity ("fatal" | "error" | "warning" | "info"):
62
62
  autter.captureMessage("Legacy /orders lookup used", "warning");
63
63
 
64
+ // named process spans — background jobs, queue consumers, cron ticks,
65
+ // DB-heavy calls. Always recorded (never head-sampled), so Autter's
66
+ // slow-process monitor sees accurate run counts and durations, and can
67
+ // flag the process when it is slow and repeating a lot:
68
+ await autter.withProcessSpan("invoice.rebuild", async () => {
69
+ await rebuildInvoices();
70
+ });
71
+
64
72
  // graceful shutdown flushes exporters:
65
73
  await autter.shutdown();
66
74
  ```
@@ -75,9 +83,24 @@ Defaults (cheap by construction):
75
83
  | Signal | Default |
76
84
  | --- | --- |
77
85
  | Captured/unhandled exceptions | 100% (dedicated always-on tracer) |
78
- | Traces | 1% head sampling (`traceSampleRate`) |
86
+ | Traces containing an error | 100% (tail retention, `retainTracesOnError`) |
87
+ | `withProcessSpan` spans | 100% (same always-on tracer) |
88
+ | LLM/GenAI call spans | 100% (`llmTracing`, on by default) |
89
+ | Healthy traces | 1% head sampling (`traceSampleRate`) |
79
90
  | Request metrics | exported every 60 s |
80
91
  | Logs | not collected |
92
+ | PII in custom attributes | redacted before export (`redactAttributes`) |
93
+ | Forgotten `shutdown()` | exporters still flushed on exit (`autoFlush`) |
94
+
95
+ **Error-linked trace retention.** Errors export at 100% while traces are
96
+ head-sampled — on its own that strands a retained error without the trace
97
+ that explains it. So unsampled spans are kept briefly in an in-process
98
+ buffer, and the moment a trace shows an error — a 5xx response, a recorded
99
+ exception, `captureException`, or an error-severity `captureMessage` — the
100
+ whole trace is exported, sampling lottery notwithstanding. The buffer is
101
+ bounded (256 spans per trace, 5 000 spans total, dropped as soon as the
102
+ request ends healthy, 30 s TTL), degrades to plain head sampling on
103
+ overflow, and never blocks. Disable with `retainTracesOnError: false`.
81
104
 
82
105
  Crashes are observed via `process.uncaughtExceptionMonitor`, which does
83
106
  **not** change your process's exit behaviour; the final flush is
@@ -88,7 +111,171 @@ import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
88
111
  initAutterServer({ ..., instrumentations: [new ExpressInstrumentation()] });
89
112
  ```
90
113
 
114
+ ### Lifecycle: never lose telemetry to a forgotten shutdown()
115
+
116
+ Telemetry is batched (errors every ~2 s, healthy traces every ~5 s, metrics
117
+ every 60 s) — so exiting without flushing loses whatever is still buffered.
118
+ `initAutterServer` therefore installs an exit flush **by default**: on
119
+ `beforeExit`, SIGINT, and SIGTERM it force-flushes every exporter, then lets
120
+ your process die as it would have (conventional 130/143 codes). If your own
121
+ code also handles those signals, Autter only flushes alongside it and never
122
+ touches your exit path. Opt out with `autoFlush: false`.
123
+
124
+ Prefer explicit control? Do the same yourself and get a handle back:
125
+
126
+ ```ts
127
+ import { installAutterAutoFlush } from "@autter/runtime-node";
128
+
129
+ const handle = installAutterAutoFlush(); // uses the active server's exporters
130
+ // …later: handle.flush("deploy-drain") or handle.dispose()
131
+ ```
132
+
133
+ If the process still exits with captures that were never confirmed
134
+ exported (e.g. a flush timed out), you get a one-line stderr warning — not
135
+ silent loss. While wiring things up, set `debug: true` (or `AUTTER_DEBUG=1`)
136
+ to see `[autter] exported N span(s)` lines on stderr as batches leave.
137
+
91
138
  Note on usage rollups: requests are counted from the `http.server.duration`
92
139
  metric (100% accurate) and additionally from sampled server spans. At the
93
140
  default 1% sampling the span contribution is negligible; if you set
94
141
  `traceSampleRate: 1` in development, expect request counts roughly doubled.
142
+ Tail-retained error traces don't distort this: their spans carry
143
+ `autter.tail_retained` and the ingester keeps them out of span-fed rollups.
144
+
145
+ ### Privacy: custom attributes are redacted before they leave the process
146
+
147
+ The browser relay whitelist-sanitises everything a client posts — but the
148
+ server tracker accepts free-form attributes from *your* code, where a stray
149
+ `captureException(err, { "user.email": … })` would otherwise go out
150
+ verbatim. Custom attributes are therefore scrubbed at capture time by
151
+ default:
152
+
153
+ - values that look like emails, JWTs, `sk-…`/`ghp_…`/AWS/Slack tokens,
154
+ `Bearer …` headers, or `scheme://user:pass@host` URLs are masked;
155
+ - attributes whose **key** looks sensitive (`password`, `token`, `secret`,
156
+ `api_key`, `authorization`, `cookie`, `ssn`, `card_number`, …) are masked
157
+ wholesale;
158
+ - non-sensitive keys and primitives pass through untouched, so grouping and
159
+ dashboards keep working.
160
+
161
+ Disable or extend it per service:
162
+
163
+ ```ts
164
+ initAutterServer({
165
+ ...,
166
+ // redactAttributes: false, // opt out entirely
167
+ redactAttributes: {
168
+ additionalKeyPatterns: ["employee_id"],
169
+ additionalValuePatterns: [/^ACC-\d+$/],
170
+ },
171
+ });
172
+ ```
173
+
174
+ Libraries that must never forward PII regardless of host configuration can
175
+ wrap once:
176
+
177
+ ```ts
178
+ import { makeSafeCapture } from "@autter/runtime-node";
179
+ const safe = makeSafeCapture();
180
+ safe.captureException(err, { "user.email": email }); // masked before export
181
+ ```
182
+
183
+ The raw primitive is exported too (`redactAttributes(attrs, options)`).
184
+ This closes the server-side gap to match the browser relay's payload
185
+ whitelist; it is best-effort scrubbing of obvious PII shapes, not a DLP
186
+ engine — keep secrets out of attributes in the first place.
187
+
188
+ ## 3. LLM tracing
189
+
190
+ `initAutterServer` initialises LLM tracing automatically: any GenAI span —
191
+ `gen_ai.*` semconv attributes or the Vercel AI SDK's `ai.*` spans — bypasses
192
+ head sampling, so **every** model call is recorded with model, tokens,
193
+ latency, and a USD cost. Opt out with `llmTracing: false`.
194
+
195
+ **Easiest: wrap the client once** — works with the OpenAI, Anthropic, and
196
+ Google GenAI SDKs (or anything with the same call shapes), streaming
197
+ included; every call through it is traced with no per-call code:
198
+
199
+ ```ts
200
+ import { instrumentLlmClient } from "@autter/runtime-node";
201
+
202
+ const openai = instrumentLlmClient(new OpenAI());
203
+ // use it exactly as before — chat, embeddings, streams are all recorded
204
+ const out = await openai.chat.completions.create({ model: "gpt-5-mini", ... });
205
+ ```
206
+
207
+ Provider is detected from the client (override with
208
+ `{ provider, userId, attributes }` as the second argument). For streamed
209
+ responses the span closes when the stream is consumed; OpenAI streams only
210
+ report token usage when you pass
211
+ `stream_options: { include_usage: true }`.
212
+
213
+ **Vercel AI SDK** — just turn on its telemetry, nothing else:
214
+
215
+ ```ts
216
+ const { text } = await generateText({
217
+ model: openai("gpt-5-mini"),
218
+ prompt,
219
+ experimental_telemetry: { isEnabled: true, metadata: { userId: user.id } },
220
+ });
221
+ ```
222
+
223
+ **Manual control** (raw fetch, unusual clients) — wrap the call:
224
+
225
+ ```ts
226
+ import { withLlmCall } from "@autter/runtime-node";
227
+
228
+ const res = await withLlmCall(
229
+ { provider: "openai", model: "gpt-5-mini", userId: user.id },
230
+ async (llm) => {
231
+ const out = await openai.chat.completions.create({ ... });
232
+ llm.setUsage({
233
+ inputTokens: out.usage?.prompt_tokens,
234
+ outputTokens: out.usage?.completion_tokens,
235
+ });
236
+ return out;
237
+ },
238
+ );
239
+ ```
240
+
241
+ Errors thrown inside are rethrown after marking the span failed — failing
242
+ model calls surface both as error issues and as `status: "error"` LLM calls.
243
+ Costs are estimated ingest-side from a built-in price table; report exact
244
+ figures with `llm.setCost(usd)` (the `autter.llm.cost_usd` attribute).
245
+
246
+ Where wrapping is awkward (queues, callbacks, batch results), report after
247
+ the fact with
248
+ `trackLlmCall({ provider, model, inputTokens, outputTokens, durationMs })`.
249
+
250
+ To verify the pipe end-to-end without calling a real model:
251
+
252
+ ```ts
253
+ import { emitLlmSelftestTrace } from "@autter/runtime-node";
254
+
255
+ const { traceId } = await emitLlmSelftestTrace();
256
+ // one fake "autter-selftest" call is flushed to the ingester; look it up by
257
+ // traceId in the dashboard's LLM tab (or runtime_llm_calls when self-hosting)
258
+ ```
259
+
260
+ ## 4. Process spans (jobs, consumers, crons)
261
+
262
+ Non-HTTP work is only visible to the slow-process monitor where a span
263
+ exists — and regular traces are 1% sampled. `withProcessSpan` records a
264
+ span **always**:
265
+
266
+ ```ts
267
+ import { withProcessSpan } from "@autter/runtime-node";
268
+
269
+ await withProcessSpan("invoice.rebuild", async () => {
270
+ await rebuildInvoices();
271
+ });
272
+ ```
273
+
274
+ Use stable, low-cardinality names; put ids in attributes
275
+ (`withProcessSpan("email.digest", fn, { "user.id": id })`).
276
+
277
+ Note on the slow-process monitor: Autter flags HTTP routes from the
278
+ unsampled request metrics, so route detection works out of the box. Non-HTTP
279
+ work is only visible where a span exists — relying on 1%-sampled regular
280
+ traces there would undercount ~100×, which is why these spans skip head
281
+ sampling.