@autter/runtime-node 1.3.0 → 1.3.2
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 +3 -0
- package/dist/index.cjs +129 -9
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +117 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,6 +187,9 @@ engine — keep secrets out of attributes in the first place.
|
|
|
187
187
|
|
|
188
188
|
## 3. LLM tracing
|
|
189
189
|
|
|
190
|
+
For the field contract, manual reporting patterns, and provider-agnostic
|
|
191
|
+
OpenTelemetry setup, see the dedicated [LLM instrumentation guide](../../docs/LLM-INSTRUMENTATION.md).
|
|
192
|
+
|
|
190
193
|
`initAutterServer` initialises LLM tracing automatically: any GenAI span —
|
|
191
194
|
`gen_ai.*` semconv attributes or the Vercel AI SDK's `ai.*` spans — bypasses
|
|
192
195
|
head sampling, so **every** model call is recorded with model, tokens,
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -30,7 +40,9 @@ __export(index_exports, {
|
|
|
30
40
|
instrumentLlmClient: () => instrumentLlmClient,
|
|
31
41
|
makeSafeCapture: () => makeSafeCapture,
|
|
32
42
|
redactAttributes: () => redactAttributes,
|
|
43
|
+
reportOutcome: () => reportOutcome,
|
|
33
44
|
sanitizeBrowserPayload: () => sanitizeBrowserPayload,
|
|
45
|
+
startCaughtExceptionSampler: () => startCaughtExceptionSampler,
|
|
34
46
|
trackLlmCall: () => trackLlmCall,
|
|
35
47
|
withLlmCall: () => withLlmCall,
|
|
36
48
|
withProcessSpan: () => withProcessSpan
|
|
@@ -68,7 +80,10 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
68
80
|
"unhandled_rejection",
|
|
69
81
|
"message",
|
|
70
82
|
"session_start",
|
|
71
|
-
"track_event"
|
|
83
|
+
"track_event",
|
|
84
|
+
"outcome",
|
|
85
|
+
"request_failure",
|
|
86
|
+
"timing"
|
|
72
87
|
]);
|
|
73
88
|
var SEVERITIES = /* @__PURE__ */ new Set(["fatal", "error", "warning", "info"]);
|
|
74
89
|
function sanitizeBrowserPayload(raw) {
|
|
@@ -90,6 +105,7 @@ function sanitizeBrowserPayload(raw) {
|
|
|
90
105
|
...typeof e.severity === "string" && SEVERITIES.has(e.severity) ? { severity: e.severity } : {},
|
|
91
106
|
message: typeof e.message === "string" ? e.message.slice(0, 4e3) : "",
|
|
92
107
|
...typeof e.name === "string" ? { name: e.name.slice(0, 200) } : {},
|
|
108
|
+
...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) ? { durationMs: Math.max(0, Math.min(12e4, e.durationMs)) } : {},
|
|
93
109
|
...typeof e.stack === "string" ? { stack: e.stack.slice(0, 32e3) } : {},
|
|
94
110
|
...typeof e.errorType === "string" ? { errorType: e.errorType.slice(0, 200) } : {},
|
|
95
111
|
...typeof e.filename === "string" ? { filename: e.filename.split("?")[0].slice(0, 1e3) } : {},
|
|
@@ -323,6 +339,7 @@ function installAutterAutoFlush(options = {}) {
|
|
|
323
339
|
const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
|
|
324
340
|
const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
|
|
325
341
|
const signals = options.signals ?? DEFAULT_SIGNALS;
|
|
342
|
+
const flushOnUncaughtException = options.flushOnUncaughtException !== false;
|
|
326
343
|
let disposed = false;
|
|
327
344
|
let inFlight = null;
|
|
328
345
|
const doFlush = async () => {
|
|
@@ -368,6 +385,19 @@ function installAutterAutoFlush(options = {}) {
|
|
|
368
385
|
return inFlight;
|
|
369
386
|
};
|
|
370
387
|
const signalCounts = /* @__PURE__ */ new Map();
|
|
388
|
+
let handlingUncaughtException = false;
|
|
389
|
+
const onUncaughtException = (error) => {
|
|
390
|
+
if (process.listenerCount("uncaughtException") > 1) return;
|
|
391
|
+
if (handlingUncaughtException) return;
|
|
392
|
+
handlingUncaughtException = true;
|
|
393
|
+
logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
394
|
+
const keepAlive = setTimeout(() => {
|
|
395
|
+
}, timeoutMs + 50);
|
|
396
|
+
void flush("uncaughtException").finally(() => {
|
|
397
|
+
clearTimeout(keepAlive);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
});
|
|
400
|
+
};
|
|
371
401
|
const onSignal = (signal) => {
|
|
372
402
|
const count = (signalCounts.get(signal) ?? 0) + 1;
|
|
373
403
|
signalCounts.set(signal, count);
|
|
@@ -407,6 +437,9 @@ function installAutterAutoFlush(options = {}) {
|
|
|
407
437
|
};
|
|
408
438
|
const signalHandlers = /* @__PURE__ */ new Map();
|
|
409
439
|
process.on("beforeExit", onBeforeExit);
|
|
440
|
+
if (flushOnUncaughtException) {
|
|
441
|
+
process.on("uncaughtException", onUncaughtException);
|
|
442
|
+
}
|
|
410
443
|
for (const signal of signals) {
|
|
411
444
|
const handler = () => onSignal(signal);
|
|
412
445
|
signalHandlers.set(signal, handler);
|
|
@@ -418,6 +451,7 @@ function installAutterAutoFlush(options = {}) {
|
|
|
418
451
|
dispose() {
|
|
419
452
|
disposed = true;
|
|
420
453
|
process.removeListener("beforeExit", onBeforeExit);
|
|
454
|
+
process.removeListener("uncaughtException", onUncaughtException);
|
|
421
455
|
process.removeListener("exit", onExit);
|
|
422
456
|
for (const [signal, handler] of signalHandlers) {
|
|
423
457
|
process.removeListener(signal, handler);
|
|
@@ -1096,13 +1130,24 @@ function initAutterServer(options) {
|
|
|
1096
1130
|
const span = reuseActive ? activeSpan : errorTracer.startSpan(isError ? error.name : "Error");
|
|
1097
1131
|
span.setAttributes({ "autter.severity": "error", ...redacted });
|
|
1098
1132
|
if (isError && error.stack) {
|
|
1099
|
-
|
|
1133
|
+
if (redacted["autter.handled"] === true) {
|
|
1134
|
+
span.addEvent("exception", {
|
|
1135
|
+
"exception.type": error.name,
|
|
1136
|
+
"exception.message": message,
|
|
1137
|
+
"exception.stacktrace": error.stack,
|
|
1138
|
+
"autter.handled": true,
|
|
1139
|
+
"autter.sampled": redacted["autter.sampled"] === true
|
|
1140
|
+
});
|
|
1141
|
+
} else {
|
|
1142
|
+
span.recordException(error);
|
|
1143
|
+
}
|
|
1100
1144
|
} else {
|
|
1101
1145
|
const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
|
|
1102
1146
|
span.addEvent("exception", {
|
|
1103
1147
|
"exception.type": isError ? error.name : "Error",
|
|
1104
1148
|
"exception.message": message,
|
|
1105
|
-
...stack ? { "exception.stacktrace": stack } : {}
|
|
1149
|
+
...stack ? { "exception.stacktrace": stack } : {},
|
|
1150
|
+
...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {}
|
|
1106
1151
|
});
|
|
1107
1152
|
}
|
|
1108
1153
|
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message });
|
|
@@ -1126,15 +1171,28 @@ function initAutterServer(options) {
|
|
|
1126
1171
|
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message });
|
|
1127
1172
|
span.end();
|
|
1128
1173
|
}
|
|
1174
|
+
function reportOutcome2(name, message, attributes) {
|
|
1175
|
+
errorTraceBuffer?.retainActiveTrace();
|
|
1176
|
+
telemetryStats.markCaptured();
|
|
1177
|
+
const activeSpan = import_api.trace.getActiveSpan();
|
|
1178
|
+
const reuseActive = activeSpan?.isRecording() === true;
|
|
1179
|
+
const span = reuseActive ? activeSpan : errorTracer.startSpan("OutcomeFailure");
|
|
1180
|
+
const safeOutcome = activeRedactor({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
|
|
1181
|
+
const safeName = String(safeOutcome["autter.outcome.name"] ?? "operation");
|
|
1182
|
+
const safeMessage = String(safeOutcome["autter.outcome.message"] ?? "failed");
|
|
1183
|
+
span.addEvent("autter.outcome", {
|
|
1184
|
+
...activeRedactor(attributes),
|
|
1185
|
+
"autter.outcome.status": "error",
|
|
1186
|
+
"autter.outcome.name": safeName,
|
|
1187
|
+
"autter.outcome.message": safeMessage
|
|
1188
|
+
});
|
|
1189
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: safeMessage });
|
|
1190
|
+
if (!reuseActive) span.end();
|
|
1191
|
+
}
|
|
1129
1192
|
if (options.captureGlobalErrors !== false) {
|
|
1130
1193
|
process.on("uncaughtExceptionMonitor", (err) => {
|
|
1131
1194
|
captureException2(err, { "autter.unhandled": true });
|
|
1132
|
-
void flushTarget.forceFlush()
|
|
1133
|
-
});
|
|
1134
|
-
process.on("unhandledRejection", (reason) => {
|
|
1135
|
-
captureException2(reason, {
|
|
1136
|
-
"autter.unhandled": true,
|
|
1137
|
-
...reason instanceof Error ? {} : { "autter.severity": "warning" }
|
|
1195
|
+
if (options.autoFlush === false) void Promise.resolve(flushTarget.forceFlush()).catch(() => {
|
|
1138
1196
|
});
|
|
1139
1197
|
});
|
|
1140
1198
|
}
|
|
@@ -1162,6 +1220,7 @@ function initAutterServer(options) {
|
|
|
1162
1220
|
const server = {
|
|
1163
1221
|
captureException: captureException2,
|
|
1164
1222
|
captureMessage: captureMessage2,
|
|
1223
|
+
reportOutcome: reportOutcome2,
|
|
1165
1224
|
withProcessSpan: (name, fn, attributes) => runWithSpan(processTracer, name, fn, activeRedactor(attributes)),
|
|
1166
1225
|
withLlmCall: (info, fn) => runLlmSpan(llmTracer, info, fn),
|
|
1167
1226
|
trackLlmCall: (call) => recordLlmCall(llmTracer, call),
|
|
@@ -1223,6 +1282,21 @@ function captureMessage(message, severity = "warning", attributes) {
|
|
|
1223
1282
|
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message });
|
|
1224
1283
|
span.end();
|
|
1225
1284
|
}
|
|
1285
|
+
function reportOutcome(name, message, attributes) {
|
|
1286
|
+
if (active) return active.reportOutcome(name, message, attributes);
|
|
1287
|
+
const span = import_api.trace.getTracer("autter-outcomes").startSpan("OutcomeFailure");
|
|
1288
|
+
const safeOutcome = redactAttributes({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
|
|
1289
|
+
const safeName = String(safeOutcome["autter.outcome.name"] ?? "operation");
|
|
1290
|
+
const safeMessage = String(safeOutcome["autter.outcome.message"] ?? "failed");
|
|
1291
|
+
span.addEvent("autter.outcome", {
|
|
1292
|
+
...redactAttributes(attributes),
|
|
1293
|
+
"autter.outcome.status": "error",
|
|
1294
|
+
"autter.outcome.name": safeName,
|
|
1295
|
+
"autter.outcome.message": safeMessage
|
|
1296
|
+
});
|
|
1297
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: safeMessage });
|
|
1298
|
+
span.end();
|
|
1299
|
+
}
|
|
1226
1300
|
function makeSafeCapture(options) {
|
|
1227
1301
|
const clean = (attributes) => attributes ? { ...redactAttributes(attributes, options) } : void 0;
|
|
1228
1302
|
return {
|
|
@@ -1472,6 +1546,50 @@ function instrumentLlmClient(client, options) {
|
|
|
1472
1546
|
}
|
|
1473
1547
|
return deepProxy(client);
|
|
1474
1548
|
}
|
|
1549
|
+
|
|
1550
|
+
// src/caught-exceptions.ts
|
|
1551
|
+
var import_node_inspector = __toESM(require("inspector"), 1);
|
|
1552
|
+
function startCaughtExceptionSampler(options = {}) {
|
|
1553
|
+
const rate = Math.max(0, Math.min(1, options.sampleRate ?? 0.01));
|
|
1554
|
+
const cap = Math.max(1, Math.min(100, options.maxPerMinute ?? 10));
|
|
1555
|
+
const session = new import_node_inspector.default.Session();
|
|
1556
|
+
let active2 = true;
|
|
1557
|
+
let minute = 0;
|
|
1558
|
+
let sent = 0;
|
|
1559
|
+
session.connect();
|
|
1560
|
+
session.on("Debugger.paused", (message) => {
|
|
1561
|
+
session.post("Debugger.resume", () => {
|
|
1562
|
+
});
|
|
1563
|
+
if (!active2 || message.params.reason !== "exception") return;
|
|
1564
|
+
const nowMinute = Math.floor(Date.now() / 6e4);
|
|
1565
|
+
if (nowMinute !== minute) {
|
|
1566
|
+
minute = nowMinute;
|
|
1567
|
+
sent = 0;
|
|
1568
|
+
}
|
|
1569
|
+
if (sent >= cap || Math.random() >= rate) return;
|
|
1570
|
+
sent++;
|
|
1571
|
+
const frames = message.params.callFrames.slice(0, 8).filter((frame) => frame.url && !frame.url.startsWith("node:")).map((frame) => ` at ${frame.functionName || "anonymous"} (${frame.url.split("?")[0]}:${frame.location.lineNumber + 1}:${(frame.location.columnNumber ?? 0) + 1})`);
|
|
1572
|
+
if (!frames.length) return;
|
|
1573
|
+
const error = new Error("Sampled caught exception");
|
|
1574
|
+
error.name = "CaughtException";
|
|
1575
|
+
error.stack = `${error.name}: ${error.message}
|
|
1576
|
+
${frames.join("\n")}`;
|
|
1577
|
+
queueMicrotask(() => {
|
|
1578
|
+
if (active2) captureException(error, { "autter.handled": true, "autter.sampled": true });
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
session.post("Debugger.enable", (error) => {
|
|
1582
|
+
if (!error && active2) session.post("Debugger.setPauseOnExceptions", { state: "all" }, () => {
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
return { stop() {
|
|
1586
|
+
if (!active2) return;
|
|
1587
|
+
active2 = false;
|
|
1588
|
+
session.post("Debugger.setPauseOnExceptions", { state: "none" }, () => {
|
|
1589
|
+
session.post("Debugger.disable", () => session.disconnect());
|
|
1590
|
+
});
|
|
1591
|
+
} };
|
|
1592
|
+
}
|
|
1475
1593
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1476
1594
|
0 && (module.exports = {
|
|
1477
1595
|
captureException,
|
|
@@ -1484,7 +1602,9 @@ function instrumentLlmClient(client, options) {
|
|
|
1484
1602
|
instrumentLlmClient,
|
|
1485
1603
|
makeSafeCapture,
|
|
1486
1604
|
redactAttributes,
|
|
1605
|
+
reportOutcome,
|
|
1487
1606
|
sanitizeBrowserPayload,
|
|
1607
|
+
startCaughtExceptionSampler,
|
|
1488
1608
|
trackLlmCall,
|
|
1489
1609
|
withLlmCall,
|
|
1490
1610
|
withProcessSpan
|
package/dist/index.d.cts
CHANGED
|
@@ -206,6 +206,8 @@ interface AutterServer {
|
|
|
206
206
|
* errors with a lower severity so aggregations can slice one dataset.
|
|
207
207
|
*/
|
|
208
208
|
captureMessage(message: string, severity?: AutterSeverity, attributes?: Attributes): void;
|
|
209
|
+
/** Report a failed result from code that returned normally. Name must be stable. */
|
|
210
|
+
reportOutcome(name: string, message: string, attributes?: Attributes): void;
|
|
209
211
|
/**
|
|
210
212
|
* Wrap a named unit of work — a background job, queue consumer, cron
|
|
211
213
|
* tick, or DB-heavy call — in an always-recorded span. Unlike regular
|
|
@@ -264,6 +266,8 @@ declare function withLlmCall<T>(info: LlmCallInfo, fn: (llm: LlmCallHandle) => T
|
|
|
264
266
|
declare function trackLlmCall(call: TrackedLlmCall): void;
|
|
265
267
|
/** Module-level convenience for warnings/info — see AutterServer.captureMessage. */
|
|
266
268
|
declare function captureMessage(message: string, severity?: AutterSeverity, attributes?: Attributes): void;
|
|
269
|
+
/** Portable OTLP `autter.outcome` event for failed results without exceptions. */
|
|
270
|
+
declare function reportOutcome(name: string, message: string, attributes?: Attributes): void;
|
|
267
271
|
/** Capture surface with guaranteed redaction, regardless of how the host app
|
|
268
272
|
* configured initAutterServer — for library authors whose calls must never
|
|
269
273
|
* forward PII even when the host disabled it. Routes to the active server
|
|
@@ -322,6 +326,8 @@ interface AutoFlushOptions {
|
|
|
322
326
|
* signals, proceeding to exit anyway). Default 3000 ms.
|
|
323
327
|
*/
|
|
324
328
|
timeoutMs?: number;
|
|
329
|
+
/** Flush after a fatal uncaught exception, then exit with Node's crash code. Default true. */
|
|
330
|
+
flushOnUncaughtException?: boolean;
|
|
325
331
|
/** Signals handled besides the defaults. Default ["SIGINT", "SIGTERM"]. */
|
|
326
332
|
signals?: string[];
|
|
327
333
|
}
|
|
@@ -342,6 +348,8 @@ interface AutoFlushHandle {
|
|
|
342
348
|
* concurrently and let your handler decide the exit — installing this
|
|
343
349
|
* never changes an existing graceful-shutdown path. A second signal
|
|
344
350
|
* always exits immediately.
|
|
351
|
+
* - uncaughtException — delays Node's fatal exit only for the same bounded
|
|
352
|
+
* flush, then exits with its default crash status (1).
|
|
345
353
|
*
|
|
346
354
|
* Idempotent per options object; `initAutterServer` installs it by default
|
|
347
355
|
* (`autoFlush: false` opts out).
|
|
@@ -375,4 +383,12 @@ interface InstrumentLlmOptions {
|
|
|
375
383
|
}
|
|
376
384
|
declare function instrumentLlmClient<T extends object>(client: T, options?: InstrumentLlmOptions): T;
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
/** Opt-in diagnostic hook. V8 pauses briefly for every throw, including handled throws. */
|
|
387
|
+
declare function startCaughtExceptionSampler(options?: {
|
|
388
|
+
sampleRate?: number;
|
|
389
|
+
maxPerMinute?: number;
|
|
390
|
+
}): {
|
|
391
|
+
stop(): void;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export { type AutoFlushHandle, type AutoFlushOptions, type AutterServer, type AutterServerOptions, type AutterSeverity, type FlushTarget, type InstrumentLlmOptions, type LlmCallHandle, type LlmCallInfo, type LlmUsage, type RedactOptions, type RelayOptions, type SafeCapture, type TrackedLlmCall, captureException, captureMessage, createBrowserRelayFetchHandler, createBrowserRelayHandler, emitLlmSelftestTrace, initAutterServer, installAutterAutoFlush, instrumentLlmClient, makeSafeCapture, redactAttributes, reportOutcome, sanitizeBrowserPayload, startCaughtExceptionSampler, trackLlmCall, withLlmCall, withProcessSpan };
|
package/dist/index.d.ts
CHANGED
|
@@ -206,6 +206,8 @@ interface AutterServer {
|
|
|
206
206
|
* errors with a lower severity so aggregations can slice one dataset.
|
|
207
207
|
*/
|
|
208
208
|
captureMessage(message: string, severity?: AutterSeverity, attributes?: Attributes): void;
|
|
209
|
+
/** Report a failed result from code that returned normally. Name must be stable. */
|
|
210
|
+
reportOutcome(name: string, message: string, attributes?: Attributes): void;
|
|
209
211
|
/**
|
|
210
212
|
* Wrap a named unit of work — a background job, queue consumer, cron
|
|
211
213
|
* tick, or DB-heavy call — in an always-recorded span. Unlike regular
|
|
@@ -264,6 +266,8 @@ declare function withLlmCall<T>(info: LlmCallInfo, fn: (llm: LlmCallHandle) => T
|
|
|
264
266
|
declare function trackLlmCall(call: TrackedLlmCall): void;
|
|
265
267
|
/** Module-level convenience for warnings/info — see AutterServer.captureMessage. */
|
|
266
268
|
declare function captureMessage(message: string, severity?: AutterSeverity, attributes?: Attributes): void;
|
|
269
|
+
/** Portable OTLP `autter.outcome` event for failed results without exceptions. */
|
|
270
|
+
declare function reportOutcome(name: string, message: string, attributes?: Attributes): void;
|
|
267
271
|
/** Capture surface with guaranteed redaction, regardless of how the host app
|
|
268
272
|
* configured initAutterServer — for library authors whose calls must never
|
|
269
273
|
* forward PII even when the host disabled it. Routes to the active server
|
|
@@ -322,6 +326,8 @@ interface AutoFlushOptions {
|
|
|
322
326
|
* signals, proceeding to exit anyway). Default 3000 ms.
|
|
323
327
|
*/
|
|
324
328
|
timeoutMs?: number;
|
|
329
|
+
/** Flush after a fatal uncaught exception, then exit with Node's crash code. Default true. */
|
|
330
|
+
flushOnUncaughtException?: boolean;
|
|
325
331
|
/** Signals handled besides the defaults. Default ["SIGINT", "SIGTERM"]. */
|
|
326
332
|
signals?: string[];
|
|
327
333
|
}
|
|
@@ -342,6 +348,8 @@ interface AutoFlushHandle {
|
|
|
342
348
|
* concurrently and let your handler decide the exit — installing this
|
|
343
349
|
* never changes an existing graceful-shutdown path. A second signal
|
|
344
350
|
* always exits immediately.
|
|
351
|
+
* - uncaughtException — delays Node's fatal exit only for the same bounded
|
|
352
|
+
* flush, then exits with its default crash status (1).
|
|
345
353
|
*
|
|
346
354
|
* Idempotent per options object; `initAutterServer` installs it by default
|
|
347
355
|
* (`autoFlush: false` opts out).
|
|
@@ -375,4 +383,12 @@ interface InstrumentLlmOptions {
|
|
|
375
383
|
}
|
|
376
384
|
declare function instrumentLlmClient<T extends object>(client: T, options?: InstrumentLlmOptions): T;
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
/** Opt-in diagnostic hook. V8 pauses briefly for every throw, including handled throws. */
|
|
387
|
+
declare function startCaughtExceptionSampler(options?: {
|
|
388
|
+
sampleRate?: number;
|
|
389
|
+
maxPerMinute?: number;
|
|
390
|
+
}): {
|
|
391
|
+
stop(): void;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export { type AutoFlushHandle, type AutoFlushOptions, type AutterServer, type AutterServerOptions, type AutterSeverity, type FlushTarget, type InstrumentLlmOptions, type LlmCallHandle, type LlmCallInfo, type LlmUsage, type RedactOptions, type RelayOptions, type SafeCapture, type TrackedLlmCall, captureException, captureMessage, createBrowserRelayFetchHandler, createBrowserRelayHandler, emitLlmSelftestTrace, initAutterServer, installAutterAutoFlush, instrumentLlmClient, makeSafeCapture, redactAttributes, reportOutcome, sanitizeBrowserPayload, startCaughtExceptionSampler, trackLlmCall, withLlmCall, withProcessSpan };
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,10 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
29
29
|
"unhandled_rejection",
|
|
30
30
|
"message",
|
|
31
31
|
"session_start",
|
|
32
|
-
"track_event"
|
|
32
|
+
"track_event",
|
|
33
|
+
"outcome",
|
|
34
|
+
"request_failure",
|
|
35
|
+
"timing"
|
|
33
36
|
]);
|
|
34
37
|
var SEVERITIES = /* @__PURE__ */ new Set(["fatal", "error", "warning", "info"]);
|
|
35
38
|
function sanitizeBrowserPayload(raw) {
|
|
@@ -51,6 +54,7 @@ function sanitizeBrowserPayload(raw) {
|
|
|
51
54
|
...typeof e.severity === "string" && SEVERITIES.has(e.severity) ? { severity: e.severity } : {},
|
|
52
55
|
message: typeof e.message === "string" ? e.message.slice(0, 4e3) : "",
|
|
53
56
|
...typeof e.name === "string" ? { name: e.name.slice(0, 200) } : {},
|
|
57
|
+
...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) ? { durationMs: Math.max(0, Math.min(12e4, e.durationMs)) } : {},
|
|
54
58
|
...typeof e.stack === "string" ? { stack: e.stack.slice(0, 32e3) } : {},
|
|
55
59
|
...typeof e.errorType === "string" ? { errorType: e.errorType.slice(0, 200) } : {},
|
|
56
60
|
...typeof e.filename === "string" ? { filename: e.filename.split("?")[0].slice(0, 1e3) } : {},
|
|
@@ -303,6 +307,7 @@ function installAutterAutoFlush(options = {}) {
|
|
|
303
307
|
const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
|
|
304
308
|
const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
|
|
305
309
|
const signals = options.signals ?? DEFAULT_SIGNALS;
|
|
310
|
+
const flushOnUncaughtException = options.flushOnUncaughtException !== false;
|
|
306
311
|
let disposed = false;
|
|
307
312
|
let inFlight = null;
|
|
308
313
|
const doFlush = async () => {
|
|
@@ -348,6 +353,19 @@ function installAutterAutoFlush(options = {}) {
|
|
|
348
353
|
return inFlight;
|
|
349
354
|
};
|
|
350
355
|
const signalCounts = /* @__PURE__ */ new Map();
|
|
356
|
+
let handlingUncaughtException = false;
|
|
357
|
+
const onUncaughtException = (error) => {
|
|
358
|
+
if (process.listenerCount("uncaughtException") > 1) return;
|
|
359
|
+
if (handlingUncaughtException) return;
|
|
360
|
+
handlingUncaughtException = true;
|
|
361
|
+
logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
362
|
+
const keepAlive = setTimeout(() => {
|
|
363
|
+
}, timeoutMs + 50);
|
|
364
|
+
void flush("uncaughtException").finally(() => {
|
|
365
|
+
clearTimeout(keepAlive);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|
|
368
|
+
};
|
|
351
369
|
const onSignal = (signal) => {
|
|
352
370
|
const count = (signalCounts.get(signal) ?? 0) + 1;
|
|
353
371
|
signalCounts.set(signal, count);
|
|
@@ -387,6 +405,9 @@ function installAutterAutoFlush(options = {}) {
|
|
|
387
405
|
};
|
|
388
406
|
const signalHandlers = /* @__PURE__ */ new Map();
|
|
389
407
|
process.on("beforeExit", onBeforeExit);
|
|
408
|
+
if (flushOnUncaughtException) {
|
|
409
|
+
process.on("uncaughtException", onUncaughtException);
|
|
410
|
+
}
|
|
390
411
|
for (const signal of signals) {
|
|
391
412
|
const handler = () => onSignal(signal);
|
|
392
413
|
signalHandlers.set(signal, handler);
|
|
@@ -398,6 +419,7 @@ function installAutterAutoFlush(options = {}) {
|
|
|
398
419
|
dispose() {
|
|
399
420
|
disposed = true;
|
|
400
421
|
process.removeListener("beforeExit", onBeforeExit);
|
|
422
|
+
process.removeListener("uncaughtException", onUncaughtException);
|
|
401
423
|
process.removeListener("exit", onExit);
|
|
402
424
|
for (const [signal, handler] of signalHandlers) {
|
|
403
425
|
process.removeListener(signal, handler);
|
|
@@ -1076,13 +1098,24 @@ function initAutterServer(options) {
|
|
|
1076
1098
|
const span = reuseActive ? activeSpan : errorTracer.startSpan(isError ? error.name : "Error");
|
|
1077
1099
|
span.setAttributes({ "autter.severity": "error", ...redacted });
|
|
1078
1100
|
if (isError && error.stack) {
|
|
1079
|
-
|
|
1101
|
+
if (redacted["autter.handled"] === true) {
|
|
1102
|
+
span.addEvent("exception", {
|
|
1103
|
+
"exception.type": error.name,
|
|
1104
|
+
"exception.message": message,
|
|
1105
|
+
"exception.stacktrace": error.stack,
|
|
1106
|
+
"autter.handled": true,
|
|
1107
|
+
"autter.sampled": redacted["autter.sampled"] === true
|
|
1108
|
+
});
|
|
1109
|
+
} else {
|
|
1110
|
+
span.recordException(error);
|
|
1111
|
+
}
|
|
1080
1112
|
} else {
|
|
1081
1113
|
const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
|
|
1082
1114
|
span.addEvent("exception", {
|
|
1083
1115
|
"exception.type": isError ? error.name : "Error",
|
|
1084
1116
|
"exception.message": message,
|
|
1085
|
-
...stack ? { "exception.stacktrace": stack } : {}
|
|
1117
|
+
...stack ? { "exception.stacktrace": stack } : {},
|
|
1118
|
+
...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {}
|
|
1086
1119
|
});
|
|
1087
1120
|
}
|
|
1088
1121
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
@@ -1106,15 +1139,28 @@ function initAutterServer(options) {
|
|
|
1106
1139
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
1107
1140
|
span.end();
|
|
1108
1141
|
}
|
|
1142
|
+
function reportOutcome2(name, message, attributes) {
|
|
1143
|
+
errorTraceBuffer?.retainActiveTrace();
|
|
1144
|
+
telemetryStats.markCaptured();
|
|
1145
|
+
const activeSpan = trace.getActiveSpan();
|
|
1146
|
+
const reuseActive = activeSpan?.isRecording() === true;
|
|
1147
|
+
const span = reuseActive ? activeSpan : errorTracer.startSpan("OutcomeFailure");
|
|
1148
|
+
const safeOutcome = activeRedactor({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
|
|
1149
|
+
const safeName = String(safeOutcome["autter.outcome.name"] ?? "operation");
|
|
1150
|
+
const safeMessage = String(safeOutcome["autter.outcome.message"] ?? "failed");
|
|
1151
|
+
span.addEvent("autter.outcome", {
|
|
1152
|
+
...activeRedactor(attributes),
|
|
1153
|
+
"autter.outcome.status": "error",
|
|
1154
|
+
"autter.outcome.name": safeName,
|
|
1155
|
+
"autter.outcome.message": safeMessage
|
|
1156
|
+
});
|
|
1157
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
|
|
1158
|
+
if (!reuseActive) span.end();
|
|
1159
|
+
}
|
|
1109
1160
|
if (options.captureGlobalErrors !== false) {
|
|
1110
1161
|
process.on("uncaughtExceptionMonitor", (err) => {
|
|
1111
1162
|
captureException2(err, { "autter.unhandled": true });
|
|
1112
|
-
void flushTarget.forceFlush()
|
|
1113
|
-
});
|
|
1114
|
-
process.on("unhandledRejection", (reason) => {
|
|
1115
|
-
captureException2(reason, {
|
|
1116
|
-
"autter.unhandled": true,
|
|
1117
|
-
...reason instanceof Error ? {} : { "autter.severity": "warning" }
|
|
1163
|
+
if (options.autoFlush === false) void Promise.resolve(flushTarget.forceFlush()).catch(() => {
|
|
1118
1164
|
});
|
|
1119
1165
|
});
|
|
1120
1166
|
}
|
|
@@ -1142,6 +1188,7 @@ function initAutterServer(options) {
|
|
|
1142
1188
|
const server = {
|
|
1143
1189
|
captureException: captureException2,
|
|
1144
1190
|
captureMessage: captureMessage2,
|
|
1191
|
+
reportOutcome: reportOutcome2,
|
|
1145
1192
|
withProcessSpan: (name, fn, attributes) => runWithSpan(processTracer, name, fn, activeRedactor(attributes)),
|
|
1146
1193
|
withLlmCall: (info, fn) => runLlmSpan(llmTracer, info, fn),
|
|
1147
1194
|
trackLlmCall: (call) => recordLlmCall(llmTracer, call),
|
|
@@ -1203,6 +1250,21 @@ function captureMessage(message, severity = "warning", attributes) {
|
|
|
1203
1250
|
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
1204
1251
|
span.end();
|
|
1205
1252
|
}
|
|
1253
|
+
function reportOutcome(name, message, attributes) {
|
|
1254
|
+
if (active) return active.reportOutcome(name, message, attributes);
|
|
1255
|
+
const span = trace.getTracer("autter-outcomes").startSpan("OutcomeFailure");
|
|
1256
|
+
const safeOutcome = redactAttributes({ "autter.outcome.name": name.slice(0, 200), "autter.outcome.message": message.slice(0, 1e3) });
|
|
1257
|
+
const safeName = String(safeOutcome["autter.outcome.name"] ?? "operation");
|
|
1258
|
+
const safeMessage = String(safeOutcome["autter.outcome.message"] ?? "failed");
|
|
1259
|
+
span.addEvent("autter.outcome", {
|
|
1260
|
+
...redactAttributes(attributes),
|
|
1261
|
+
"autter.outcome.status": "error",
|
|
1262
|
+
"autter.outcome.name": safeName,
|
|
1263
|
+
"autter.outcome.message": safeMessage
|
|
1264
|
+
});
|
|
1265
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
|
|
1266
|
+
span.end();
|
|
1267
|
+
}
|
|
1206
1268
|
function makeSafeCapture(options) {
|
|
1207
1269
|
const clean = (attributes) => attributes ? { ...redactAttributes(attributes, options) } : void 0;
|
|
1208
1270
|
return {
|
|
@@ -1455,6 +1517,50 @@ function instrumentLlmClient(client, options) {
|
|
|
1455
1517
|
}
|
|
1456
1518
|
return deepProxy(client);
|
|
1457
1519
|
}
|
|
1520
|
+
|
|
1521
|
+
// src/caught-exceptions.ts
|
|
1522
|
+
import inspector from "inspector";
|
|
1523
|
+
function startCaughtExceptionSampler(options = {}) {
|
|
1524
|
+
const rate = Math.max(0, Math.min(1, options.sampleRate ?? 0.01));
|
|
1525
|
+
const cap = Math.max(1, Math.min(100, options.maxPerMinute ?? 10));
|
|
1526
|
+
const session = new inspector.Session();
|
|
1527
|
+
let active2 = true;
|
|
1528
|
+
let minute = 0;
|
|
1529
|
+
let sent = 0;
|
|
1530
|
+
session.connect();
|
|
1531
|
+
session.on("Debugger.paused", (message) => {
|
|
1532
|
+
session.post("Debugger.resume", () => {
|
|
1533
|
+
});
|
|
1534
|
+
if (!active2 || message.params.reason !== "exception") return;
|
|
1535
|
+
const nowMinute = Math.floor(Date.now() / 6e4);
|
|
1536
|
+
if (nowMinute !== minute) {
|
|
1537
|
+
minute = nowMinute;
|
|
1538
|
+
sent = 0;
|
|
1539
|
+
}
|
|
1540
|
+
if (sent >= cap || Math.random() >= rate) return;
|
|
1541
|
+
sent++;
|
|
1542
|
+
const frames = message.params.callFrames.slice(0, 8).filter((frame) => frame.url && !frame.url.startsWith("node:")).map((frame) => ` at ${frame.functionName || "anonymous"} (${frame.url.split("?")[0]}:${frame.location.lineNumber + 1}:${(frame.location.columnNumber ?? 0) + 1})`);
|
|
1543
|
+
if (!frames.length) return;
|
|
1544
|
+
const error = new Error("Sampled caught exception");
|
|
1545
|
+
error.name = "CaughtException";
|
|
1546
|
+
error.stack = `${error.name}: ${error.message}
|
|
1547
|
+
${frames.join("\n")}`;
|
|
1548
|
+
queueMicrotask(() => {
|
|
1549
|
+
if (active2) captureException(error, { "autter.handled": true, "autter.sampled": true });
|
|
1550
|
+
});
|
|
1551
|
+
});
|
|
1552
|
+
session.post("Debugger.enable", (error) => {
|
|
1553
|
+
if (!error && active2) session.post("Debugger.setPauseOnExceptions", { state: "all" }, () => {
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
return { stop() {
|
|
1557
|
+
if (!active2) return;
|
|
1558
|
+
active2 = false;
|
|
1559
|
+
session.post("Debugger.setPauseOnExceptions", { state: "none" }, () => {
|
|
1560
|
+
session.post("Debugger.disable", () => session.disconnect());
|
|
1561
|
+
});
|
|
1562
|
+
} };
|
|
1563
|
+
}
|
|
1458
1564
|
export {
|
|
1459
1565
|
captureException,
|
|
1460
1566
|
captureMessage,
|
|
@@ -1466,7 +1572,9 @@ export {
|
|
|
1466
1572
|
instrumentLlmClient,
|
|
1467
1573
|
makeSafeCapture,
|
|
1468
1574
|
redactAttributes,
|
|
1575
|
+
reportOutcome,
|
|
1469
1576
|
sanitizeBrowserPayload,
|
|
1577
|
+
startCaughtExceptionSampler,
|
|
1470
1578
|
trackLlmCall,
|
|
1471
1579
|
withLlmCall,
|
|
1472
1580
|
withProcessSpan
|
package/package.json
CHANGED