@autter/runtime-node 1.3.1 → 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 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
- span.recordException(error);
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
- 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, sanitizeBrowserPayload, trackLlmCall, withLlmCall, withProcessSpan };
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
- 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, sanitizeBrowserPayload, trackLlmCall, withLlmCall, withProcessSpan };
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
- span.recordException(error);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autter/runtime-node",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Autter Runtime for Node.js: same-origin browser relay handler + curated OpenTelemetry server tracker",
5
5
  "license": "MIT",
6
6
  "type": "module",