@autter/runtime-node 1.5.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/dist/index.js CHANGED
@@ -2,6 +2,8 @@ import {
2
2
  CODE_PATTERN,
3
3
  DEFAULT_REQUEST_ID_HEADER,
4
4
  RuntimeError,
5
+ addBreadcrumb,
6
+ breadcrumbEventAttributes,
5
7
  captureExceptionAsLog,
6
8
  compileIgnore,
7
9
  configureRuntimeLogger,
@@ -15,7 +17,10 @@ import {
15
17
  errorCodeOf,
16
18
  errorInternal,
17
19
  fileSink,
20
+ fingerprintEventAttributes,
21
+ fingerprintValues,
18
22
  flushRuntimeLogs,
23
+ isError,
19
24
  isRuntimeErrorLike,
20
25
  local,
21
26
  makeRedactor,
@@ -35,13 +40,15 @@ import {
35
40
  runtimeContext,
36
41
  runtimeLogStats,
37
42
  runtimeLogger,
43
+ sanitizeBreadcrumbs,
38
44
  shutdownRuntimeLogger,
45
+ splitFingerprint,
39
46
  toClientError,
40
47
  withRuntimeOperation
41
- } from "./chunk-3TJHUCZ6.js";
48
+ } from "./chunk-FBKYOHU2.js";
42
49
  import {
43
50
  runDoctor
44
- } from "./chunk-ZS4WQJQR.js";
51
+ } from "./chunk-Y4T7SQMO.js";
45
52
  import {
46
53
  COMPAT_MANIFEST,
47
54
  CountingExporter,
@@ -62,7 +69,7 @@ import {
62
69
  setDebugMode,
63
70
  telemetryStats,
64
71
  unregisterFlushTargets
65
- } from "./chunk-L7HWKMCZ.js";
72
+ } from "./chunk-OY2HTUWY.js";
66
73
 
67
74
  // src/relay.ts
68
75
  var IpWindow = class {
@@ -102,6 +109,7 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
102
109
  "timing"
103
110
  ]);
104
111
  var SEVERITIES = /* @__PURE__ */ new Set(["fatal", "error", "warning", "info"]);
112
+ var TRACE_ID = /^(?!0{32})[0-9a-f]{32}$/;
105
113
  var CONTEXT_MAX_DEPTH = 6;
106
114
  var CONTEXT_MAX_NODES = 256;
107
115
  var CONTEXT_MAX_STRING = 4e3;
@@ -214,6 +222,27 @@ function relayContext(raw, redactor) {
214
222
  const bounded = boundContext(raw);
215
223
  return Array.isArray(bounded) ? {} : { context: redactor(bounded) };
216
224
  }
225
+ function relayBreadcrumbs(raw, redactor) {
226
+ const breadcrumbs = sanitizeBreadcrumbs(raw, redactor);
227
+ return breadcrumbs ? { breadcrumbs } : {};
228
+ }
229
+ var DEBUG_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
230
+ function relayDebugIds(raw, redactor) {
231
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
232
+ const debugIds = {};
233
+ let count = 0;
234
+ for (const [url, id] of Object.entries(raw)) {
235
+ if (count >= 50) break;
236
+ if (typeof id !== "string" || !DEBUG_ID_RE.test(id) || url.length > 1e3) continue;
237
+ debugIds[redactor.text(url.split(/[?#]/)[0])] = id.toLowerCase();
238
+ count++;
239
+ }
240
+ return count ? { debugIds } : {};
241
+ }
242
+ function relayFingerprint(raw, redactor) {
243
+ const values = fingerprintValues(raw, redactor.text);
244
+ return values ? { fingerprint: Array.isArray(raw) ? values : values[0] } : {};
245
+ }
217
246
  var defaultRelayRedactor = makeRedactor(true);
218
247
  function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
219
248
  if (typeof raw !== "object" || raw === null) return null;
@@ -241,7 +270,11 @@ function sanitizeBrowserPayload(raw, redactor = defaultRelayRedactor) {
241
270
  ...typeof e.line === "number" ? { line: e.line } : {},
242
271
  ...typeof e.column === "number" ? { column: e.column } : {},
243
272
  ...typeof e.route === "string" ? { route: e.route.split("?")[0].slice(0, 1e3) } : {},
244
- ...typeof e.context === "object" && e.context !== null ? relayContext(e.context, redactor) : {}
273
+ ...typeof e.context === "object" && e.context !== null ? relayContext(e.context, redactor) : {},
274
+ ...relayBreadcrumbs(e.breadcrumbs, redactor),
275
+ ...relayDebugIds(e.debugIds, redactor),
276
+ ...relayFingerprint(e.fingerprint, redactor),
277
+ ...typeof e.traceId === "string" && TRACE_ID.test(e.traceId) ? { traceId: e.traceId } : {}
245
278
  });
246
279
  }
247
280
  return {
@@ -275,6 +308,9 @@ function forward(payload, opts) {
275
308
  );
276
309
  observeIngesterResponse({ status: response.status, headers: response.headers });
277
310
  for (const feature of featuresForBrowserEvents(types)) noteCompatFeature(feature);
311
+ if ((payload.events ?? []).some((e) => e.traceId)) {
312
+ noteCompatFeature("browser_trace_propagation");
313
+ }
278
314
  noteRelayRejection(types, response.status, response.headers);
279
315
  }).catch((err) => {
280
316
  (opts.onError ?? ((e) => console.warn("autter relay forward failed:", e)))(
@@ -413,7 +449,7 @@ import {
413
449
  SpanStatusCode,
414
450
  TraceFlags
415
451
  } from "@opentelemetry/api";
416
- import { getRPCMetadata, RPCType } from "@opentelemetry/core";
452
+ import { getRPCMetadata as getRPCMetadata2, RPCType as RPCType2 } from "@opentelemetry/core";
417
453
  import {
418
454
  AggregationTemporalityPreference,
419
455
  OTLPMetricExporter
@@ -602,6 +638,7 @@ function startMemoryMetrics() {
602
638
  // src/requests.ts
603
639
  import { AsyncResource } from "async_hooks";
604
640
  import { context, trace } from "@opentelemetry/api";
641
+ import { getRPCMetadata, RPCType } from "@opentelemetry/core";
605
642
  var REQUEST_STATE = /* @__PURE__ */ Symbol.for("autter.runtime.request");
606
643
  var BOUND = /* @__PURE__ */ Symbol.for("autter.runtime.bound");
607
644
  var CAPTURED = /* @__PURE__ */ new WeakSet();
@@ -610,6 +647,11 @@ var capturer = () => {
610
647
  function setRequestExceptionCapturer(fn) {
611
648
  capturer = fn;
612
649
  }
650
+ var annotator = () => {
651
+ };
652
+ function setRequestSpanAnnotator(fn) {
653
+ annotator = fn;
654
+ }
613
655
  function markCaptured(error) {
614
656
  if (error && typeof error === "object") CAPTURED.add(error);
615
657
  }
@@ -624,17 +666,48 @@ function expressRouteOf(req) {
624
666
  function defaultShouldCapture(error) {
625
667
  return error instanceof RuntimeError || errorCodeOf(error) !== void 0 || responseStatusOf(error) >= 500;
626
668
  }
627
- function captureAtBoundary(error, state, rule = void 0) {
628
- const wanted = typeof rule === "function" ? rule(error) : rule === void 0 ? defaultShouldCapture(error) : rule;
669
+ function onRequestSpan(fn) {
670
+ const active2 = context.active();
671
+ const metadata = getRPCMetadata(active2);
672
+ const span = metadata?.type === RPCType.HTTP ? metadata.span : void 0;
673
+ if (!span || !span.isRecording() || trace.getSpan(active2) === span) {
674
+ fn();
675
+ return;
676
+ }
677
+ context.with(trace.setSpan(active2, span), fn);
678
+ }
679
+ function captureAtBoundary(error, state, rule = void 0, attributes) {
680
+ let wanted;
681
+ try {
682
+ wanted = typeof rule === "function" ? rule(error) : rule === void 0 ? defaultShouldCapture(error) : rule;
683
+ } catch {
684
+ wanted = false;
685
+ }
629
686
  if (!wanted) return;
630
687
  if (error && typeof error === "object" && CAPTURED.has(error)) return;
631
688
  markCaptured(error);
632
689
  try {
633
- if (state && !state.sealed) local.run(state, () => capturer(error));
634
- else capturer(error);
690
+ const run = () => onRequestSpan(() => capturer(error, attributes));
691
+ if (state && !state.sealed) local.run(state, run);
692
+ else run();
635
693
  } catch {
636
694
  }
637
695
  }
696
+ function requestStateOf(req) {
697
+ if (!req || typeof req !== "object") return void 0;
698
+ return req[REQUEST_STATE];
699
+ }
700
+ function captureRequestError(error, attributes) {
701
+ if (error && typeof error === "object" && CAPTURED.has(error)) {
702
+ if (!attributes) return;
703
+ try {
704
+ onRequestSpan(() => annotator(attributes));
705
+ } catch {
706
+ }
707
+ return;
708
+ }
709
+ captureAtBoundary(error, currentOperation(), true, attributes);
710
+ }
638
711
  function bindEmitter(emitter, state) {
639
712
  const target = emitter;
640
713
  if (!target || typeof target !== "object" || target[BOUND]) return;
@@ -976,6 +1049,132 @@ function startHookRequest(res) {
976
1049
  local.enterWith(state);
977
1050
  }
978
1051
 
1052
+ // src/unhandled-rejections.ts
1053
+ var MODES = /* @__PURE__ */ new Set([
1054
+ "throw",
1055
+ "strict",
1056
+ "warn",
1057
+ "warn-with-error-code",
1058
+ "none"
1059
+ ]);
1060
+ var LISTENER_MARK = /* @__PURE__ */ Symbol.for("autter.runtime.unhandledRejection.listener");
1061
+ var HANDLED_KEY = /* @__PURE__ */ Symbol.for("autter.runtime.unhandledRejection.handled");
1062
+ function handledPromises() {
1063
+ const registry = globalThis;
1064
+ let set = registry[HANDLED_KEY];
1065
+ if (!set) {
1066
+ set = /* @__PURE__ */ new WeakSet();
1067
+ registry[HANDLED_KEY] = set;
1068
+ }
1069
+ return set;
1070
+ }
1071
+ var recorded = /* @__PURE__ */ new WeakSet();
1072
+ function markRecorded(reason) {
1073
+ if (!reason || typeof reason !== "object") return;
1074
+ recorded.add(reason);
1075
+ setImmediate(() => recorded.delete(reason)).unref?.();
1076
+ }
1077
+ function recordedAsRejection(error) {
1078
+ return !!error && typeof error === "object" && recorded.has(error);
1079
+ }
1080
+ function splitOptions(value) {
1081
+ return value ? value.split(/\s+/).filter(Boolean) : [];
1082
+ }
1083
+ function unhandledRejectionsMode(execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS) {
1084
+ const args = [...splitOptions(nodeOptions), ...execArgv];
1085
+ let mode = "throw";
1086
+ for (let i = 0; i < args.length; i++) {
1087
+ const arg = args[i] ?? "";
1088
+ if (!arg.startsWith("--")) continue;
1089
+ const eq = arg.indexOf("=");
1090
+ const name = (eq < 0 ? arg : arg.slice(0, eq)).replace(/_/g, "-");
1091
+ if (name !== "--unhandled-rejections") continue;
1092
+ const value = eq < 0 ? args[++i] : arg.slice(eq + 1);
1093
+ if (value && MODES.has(value))
1094
+ mode = value;
1095
+ }
1096
+ return mode;
1097
+ }
1098
+ function isErrorLike(value) {
1099
+ try {
1100
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "stack");
1101
+ } catch {
1102
+ return false;
1103
+ }
1104
+ }
1105
+ function describeReason(reason) {
1106
+ if (typeof reason !== "object" && typeof reason !== "function" || reason === null) {
1107
+ try {
1108
+ return String(reason);
1109
+ } catch {
1110
+ return typeof reason;
1111
+ }
1112
+ }
1113
+ try {
1114
+ const proto = Object.getPrototypeOf(reason);
1115
+ const ctor = proto?.constructor;
1116
+ const name = typeof ctor === "function" ? ctor.name : "";
1117
+ return name ? `#<${name}>` : "[object Object]";
1118
+ } catch {
1119
+ return "[object Object]";
1120
+ }
1121
+ }
1122
+ function toUncaught(reason, from) {
1123
+ if (isErrorLike(reason)) return reason;
1124
+ const error = new Error(
1125
+ `This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "${describeReason(reason)}".`
1126
+ );
1127
+ error.name = "UnhandledPromiseRejection";
1128
+ error.code = "ERR_UNHANDLED_REJECTION";
1129
+ Error.captureStackTrace?.(error, from);
1130
+ return error;
1131
+ }
1132
+ var warningId = 0;
1133
+ function emitNodeWarning(reason) {
1134
+ try {
1135
+ const text = isErrorLike(reason) ? String(reason.stack) : describeReason(reason);
1136
+ process.emitWarning(text, "UnhandledPromiseRejectionWarning");
1137
+ process.emitWarning(
1138
+ `Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag \`--unhandled-rejections=strict\` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: ${++warningId})`,
1139
+ "UnhandledPromiseRejectionWarning"
1140
+ );
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ function installUnhandledRejectionCapture(capture, mode = unhandledRejectionsMode()) {
1145
+ if (mode === "strict") return () => {
1146
+ };
1147
+ const listener = (reason, promise) => {
1148
+ try {
1149
+ if (promise && typeof promise === "object") {
1150
+ const handled = handledPromises();
1151
+ if (handled.has(promise)) return;
1152
+ handled.add(promise);
1153
+ }
1154
+ const alone = process.listeners("unhandledRejection").every((fn) => fn[LISTENER_MARK] === true);
1155
+ if (alone && mode === "throw") {
1156
+ const error = toUncaught(reason, listener);
1157
+ process.nextTick(() => {
1158
+ throw error;
1159
+ });
1160
+ return;
1161
+ }
1162
+ if (alone && mode === "warn-with-error-code") {
1163
+ emitNodeWarning(reason);
1164
+ process.exitCode = 1;
1165
+ }
1166
+ markRecorded(reason);
1167
+ capture(reason);
1168
+ } catch {
1169
+ }
1170
+ };
1171
+ listener[LISTENER_MARK] = true;
1172
+ process.on("unhandledRejection", listener);
1173
+ return () => {
1174
+ process.off("unhandledRejection", listener);
1175
+ };
1176
+ }
1177
+
979
1178
  // src/server.ts
980
1179
  var LLM_MARKER_ATTRIBUTES = [
981
1180
  "gen_ai.operation.name",
@@ -1225,7 +1424,7 @@ async function runLlmSpan(tracer, info, fn) {
1225
1424
  return result;
1226
1425
  } catch (err) {
1227
1426
  recordError(span, err);
1228
- if (err instanceof Error) span.setAttribute("error.type", err.name);
1427
+ if (isError(err)) span.setAttribute("error.type", err.name);
1229
1428
  throw err;
1230
1429
  } finally {
1231
1430
  span.end();
@@ -1247,7 +1446,7 @@ function recordLlmCall(tracer, call) {
1247
1446
  );
1248
1447
  if (call.error !== void 0 && call.error !== null) {
1249
1448
  recordError(span, call.error);
1250
- if (call.error instanceof Error) {
1449
+ if (isError(call.error)) {
1251
1450
  span.setAttribute("error.type", call.error.name);
1252
1451
  }
1253
1452
  } else {
@@ -1275,13 +1474,14 @@ async function runWithSpan(tracer, name, fn, attributes, links) {
1275
1474
  }
1276
1475
  }
1277
1476
  function recordError(span, err) {
1278
- const isError = err instanceof Error;
1279
- const message = activeRedactor.text(isError ? err.message : String(err));
1280
- if (isError) {
1477
+ const isErr = isError(err);
1478
+ const message = activeRedactor.text(isErr ? err.message : String(err));
1479
+ if (isErr) {
1281
1480
  span.addEvent("exception", {
1282
1481
  "exception.type": err.name,
1283
1482
  "exception.message": message,
1284
- ...err.stack ? { "exception.stacktrace": activeRedactor.text(err.stack) } : {}
1483
+ ...err.stack ? { "exception.stacktrace": activeRedactor.text(err.stack) } : {},
1484
+ ...breadcrumbEventAttributes(activeRedactor.text, err)
1285
1485
  });
1286
1486
  }
1287
1487
  span.setStatus({ code: SpanStatusCode.ERROR, message });
@@ -1291,8 +1491,8 @@ function redactWithActive(attributes) {
1291
1491
  }
1292
1492
  function captureExpressRoute(_span, response) {
1293
1493
  if (!(response instanceof ServerResponse)) return;
1294
- const rpcMetadata = getRPCMetadata(context2.active());
1295
- if (rpcMetadata?.type !== RPCType.HTTP) return;
1494
+ const rpcMetadata = getRPCMetadata2(context2.active());
1495
+ if (rpcMetadata?.type !== RPCType2.HTTP) return;
1296
1496
  const setRoute = () => {
1297
1497
  if (rpcMetadata.route) return;
1298
1498
  const route = expressRouteOf(response.req);
@@ -1352,8 +1552,15 @@ function initAutterServer(options) {
1352
1552
  // would double-count every sampled request).
1353
1553
  "autter.metrics_wired": true
1354
1554
  });
1555
+ const rootSampler = new TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01);
1355
1556
  const headSampler = new ParentBasedSampler({
1356
- root: new TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01)
1557
+ root: rootSampler,
1558
+ // Browsers (runtime-browser's traceparent) send unsampled parents: they
1559
+ // make no sampling decision. Apply this service's own rate to such
1560
+ // requests instead of never head-sampling them. The ratio sampler is
1561
+ // deterministic by trace id, so services sharing a rate still agree
1562
+ // with an upstream service that chose not to sample.
1563
+ remoteParentNotSampled: rootSampler
1357
1564
  });
1358
1565
  const retainOnError = options.retainTracesOnError !== false;
1359
1566
  const slowThresholdMs = Number.isFinite(options.retainTracesAboveMs) ? Math.max(0, options.retainTracesAboveMs) : 0;
@@ -1420,18 +1627,22 @@ function initAutterServer(options) {
1420
1627
  function captureException2(error, attributes) {
1421
1628
  errorTraceBuffer?.retainActiveTrace();
1422
1629
  telemetryStats.markCaptured();
1423
- const isError = error instanceof Error;
1424
- const message = activeRedactor.text(isError ? error.message : String(error));
1425
- const stackText = isError && error.stack ? activeRedactor.text(error.stack) : "";
1426
- const redacted = activeRedactor(attributes);
1630
+ const isErr = isError(error);
1631
+ const message = activeRedactor.text(isErr ? error.message : String(error));
1632
+ const stackText = isErr && error.stack ? activeRedactor.text(error.stack) : "";
1633
+ const split = splitFingerprint(attributes);
1634
+ const fingerprint = fingerprintEventAttributes(split.fingerprint, activeRedactor.text);
1635
+ if (fingerprint["autter.fingerprint"]) noteCompatFeature("fingerprint_override");
1636
+ const redacted = activeRedactor(split.attributes);
1427
1637
  const activeSpan = trace2.getActiveSpan();
1428
1638
  const reuseActive = activeSpan?.isRecording() === true;
1429
- const span = reuseActive ? activeSpan : errorTracer.startSpan(isError ? error.name : "Error");
1639
+ const span = reuseActive ? activeSpan : errorTracer.startSpan(isErr ? error.name : "Error");
1430
1640
  const structured = structuredErrorAttributes(error);
1431
1641
  const causes = errorAttributes(error);
1432
- const declared = Object.fromEntries(
1433
- Object.entries(causes).filter(([key]) => key !== "autter.error.internal")
1434
- );
1642
+ const declared = {
1643
+ ...Object.fromEntries(Object.entries(causes).filter(([key]) => key !== "autter.error.internal")),
1644
+ ...fingerprint
1645
+ };
1435
1646
  const requestId = currentOperation()?.requestId;
1436
1647
  span.setAttributes({
1437
1648
  "autter.severity": "error",
@@ -1441,23 +1652,25 @@ function initAutterServer(options) {
1441
1652
  });
1442
1653
  markCaptured(error);
1443
1654
  noteOperationError(error);
1444
- notifyException(error, redacted);
1445
- if (isError && stackText) {
1655
+ notifyException(error, { ...redacted, ...fingerprint });
1656
+ if (isErr && stackText) {
1446
1657
  span.addEvent("exception", {
1447
1658
  "exception.type": error.name,
1448
1659
  "exception.message": message,
1449
1660
  "exception.stacktrace": stackText,
1450
1661
  ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1451
- ...declared
1662
+ ...declared,
1663
+ ...breadcrumbEventAttributes(activeRedactor.text, error)
1452
1664
  });
1453
1665
  } else {
1454
1666
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
1455
1667
  span.addEvent("exception", {
1456
- "exception.type": isError ? error.name : "Error",
1668
+ "exception.type": isErr ? error.name : "Error",
1457
1669
  "exception.message": message,
1458
1670
  ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1459
1671
  ...redacted["autter.handled"] === true ? { "autter.handled": true, "autter.sampled": redacted["autter.sampled"] === true } : {},
1460
- ...declared
1672
+ ...declared,
1673
+ ...breadcrumbEventAttributes(activeRedactor.text, error)
1461
1674
  });
1462
1675
  }
1463
1676
  span.setStatus({ code: SpanStatusCode.ERROR, message });
@@ -1477,7 +1690,8 @@ function initAutterServer(options) {
1477
1690
  "exception.type": "Message",
1478
1691
  "exception.message": safeMessage,
1479
1692
  ...stack ? { "exception.stacktrace": activeRedactor.text(stack) } : {},
1480
- "autter.severity": severity
1693
+ "autter.severity": severity,
1694
+ ...breadcrumbEventAttributes(activeRedactor.text)
1481
1695
  });
1482
1696
  span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1483
1697
  span.end();
@@ -1513,12 +1727,21 @@ function initAutterServer(options) {
1513
1727
  reportOutcome: reportOutcome2
1514
1728
  });
1515
1729
  hookRequests = options.logging?.requests === true;
1730
+ let onCrash = null;
1731
+ let uninstallRejections = null;
1516
1732
  if (options.captureGlobalErrors !== false) {
1517
- process.on("uncaughtExceptionMonitor", (err) => {
1733
+ onCrash = (err) => {
1734
+ if (recordedAsRejection(err)) return;
1518
1735
  captureException2(err, { "autter.unhandled": true });
1519
1736
  if (options.autoFlush === false) void Promise.resolve(flushTarget.forceFlush()).catch(() => {
1520
1737
  });
1521
- });
1738
+ };
1739
+ process.on("uncaughtExceptionMonitor", onCrash);
1740
+ if (options.captureUnhandledRejections !== false) {
1741
+ uninstallRejections = installUnhandledRejectionCapture(
1742
+ (reason) => captureException2(reason, { "autter.unhandled_rejection": true })
1743
+ );
1744
+ }
1522
1745
  }
1523
1746
  const flushTarget = {
1524
1747
  forceFlush: async () => {
@@ -1552,6 +1775,8 @@ function initAutterServer(options) {
1552
1775
  shutdown: async () => {
1553
1776
  const logShutdown = await Promise.allSettled([shutdownRuntimeLogger()]);
1554
1777
  hookRequests = false;
1778
+ if (onCrash) process.off("uncaughtExceptionMonitor", onCrash);
1779
+ uninstallRejections?.();
1555
1780
  stopMemoryMetrics?.();
1556
1781
  active = null;
1557
1782
  activeAlwaysOnProvider = null;
@@ -1578,9 +1803,11 @@ function captureException(error, attributes) {
1578
1803
  }
1579
1804
  markCaptured(error);
1580
1805
  noteOperationError(error);
1581
- notifyException(error, attributes);
1582
- const span = trace2.getTracer("autter-errors").startSpan(error instanceof Error ? error.name : "Error", {
1583
- attributes: activeRedactor(attributes)
1806
+ const split = splitFingerprint(attributes);
1807
+ const fingerprint = fingerprintEventAttributes(split.fingerprint, activeRedactor.text);
1808
+ notifyException(error, { ...split.attributes, ...fingerprint });
1809
+ const span = trace2.getTracer("autter-errors").startSpan(isError(error) ? error.name : "Error", {
1810
+ attributes: { ...activeRedactor(split.attributes), ...fingerprint }
1584
1811
  });
1585
1812
  recordError(span, error);
1586
1813
  span.end();
@@ -1592,7 +1819,11 @@ function setLoggerOnlyCapture(fn) {
1592
1819
  function serverActive() {
1593
1820
  return active !== null;
1594
1821
  }
1595
- setRequestExceptionCapturer((error) => captureException(error));
1822
+ setRequestExceptionCapturer((error, attributes) => captureException(error, attributes));
1823
+ setRequestSpanAnnotator((attributes) => {
1824
+ const span = trace2.getActiveSpan();
1825
+ if (span?.isRecording()) span.setAttributes(activeRedactor(splitFingerprint(attributes).attributes));
1826
+ });
1596
1827
  function withProcessSpan(name, fn, attributes) {
1597
1828
  if (active) return active.withProcessSpan(name, fn, attributes);
1598
1829
  return runWithSpan(
@@ -1625,7 +1856,8 @@ function captureMessage(message, severity = "warning", attributes) {
1625
1856
  span.addEvent("exception", {
1626
1857
  "exception.type": "Message",
1627
1858
  "exception.message": safeMessage,
1628
- "autter.severity": severity
1859
+ "autter.severity": severity,
1860
+ ...breadcrumbEventAttributes(activeRedactor.text)
1629
1861
  });
1630
1862
  span.setStatus({ code: SpanStatusCode.ERROR, message: safeMessage });
1631
1863
  span.end();
@@ -1955,6 +2187,42 @@ ${frames.join("\n")}`;
1955
2187
  } };
1956
2188
  }
1957
2189
 
2190
+ // src/framework-errors.ts
2191
+ function recordBoundaryError(error, req, rule) {
2192
+ try {
2193
+ const state = requestStateOf(req);
2194
+ if (state) noteOperationError(error, false, state);
2195
+ captureAtBoundary(error, state, rule);
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ function autterErrorHandler(options = {}) {
2200
+ return function autterErrorHandlerMiddleware(error, req, _res, next) {
2201
+ recordBoundaryError(error, req, options.capture);
2202
+ next(error);
2203
+ };
2204
+ }
2205
+ function autterFastifyErrors(fastify, options, done) {
2206
+ const rule = options?.capture;
2207
+ fastify.addHook("onError", ((request, _reply, error, next) => {
2208
+ recordBoundaryError(error, request?.raw, rule);
2209
+ next();
2210
+ }));
2211
+ done?.();
2212
+ }
2213
+ autterFastifyErrors[/* @__PURE__ */ Symbol.for("skip-override")] = true;
2214
+ autterFastifyErrors[/* @__PURE__ */ Symbol.for("fastify.display-name")] = "autter-runtime-errors";
2215
+ function autterKoa(options = {}) {
2216
+ return async function autterKoaMiddleware(ctx, next) {
2217
+ try {
2218
+ await next();
2219
+ } catch (error) {
2220
+ recordBoundaryError(error, ctx?.req, options.capture);
2221
+ throw error;
2222
+ }
2223
+ };
2224
+ }
2225
+
1958
2226
  // src/logging-only.ts
1959
2227
  import {
1960
2228
  SpanStatusCode as SpanStatusCode3,
@@ -1982,14 +2250,16 @@ function initAutterLogging(options) {
1982
2250
  debugLog(`initialising logger-only service=${options.service} endpoint=${endpoint}`);
1983
2251
  function captureException2(error, attributes) {
1984
2252
  markCaptured(error);
1985
- const redacted = redact(attributes);
2253
+ const split = splitFingerprint(attributes);
2254
+ const fingerprint = fingerprintEventAttributes(split.fingerprint, redact.text);
2255
+ const redacted = redact(split.attributes);
1986
2256
  const span = trace3.getActiveSpan();
1987
2257
  if (mode === "auto" && span?.isRecording()) {
1988
2258
  noteOperationError(error);
1989
- notifyException(error, redacted);
2259
+ notifyException(error, { ...redacted, ...fingerprint });
1990
2260
  span.setAttributes({ "autter.severity": "error", ...redacted, ...structuredErrorAttributes(error) });
1991
- const declared = errorAttributes(error);
1992
- if (error instanceof Error) {
2261
+ const declared = { ...errorAttributes(error), ...fingerprint };
2262
+ if (isError(error)) {
1993
2263
  span.addEvent("exception", {
1994
2264
  "exception.type": error.name,
1995
2265
  "exception.message": error.message,
@@ -2005,12 +2275,12 @@ function initAutterLogging(options) {
2005
2275
  }
2006
2276
  span.setStatus({
2007
2277
  code: SpanStatusCode3.ERROR,
2008
- message: error instanceof Error ? error.message : String(error)
2278
+ message: isError(error) ? error.message : String(error)
2009
2279
  });
2010
2280
  return;
2011
2281
  }
2012
- notifyException(error, redacted);
2013
- captureExceptionAsLog(error, redacted);
2282
+ notifyException(error, { ...redacted, ...fingerprint });
2283
+ captureExceptionAsLog(error, { ...redacted, ...fingerprint });
2014
2284
  }
2015
2285
  const captured = /* @__PURE__ */ new WeakSet();
2016
2286
  configureRuntimeLogger({
@@ -2037,9 +2307,18 @@ function initAutterLogging(options) {
2037
2307
  }
2038
2308
  });
2039
2309
  setLoggerOnlyCapture(captureException2);
2040
- const onCrash = (error) => captureException2(error, { "autter.unhandled": true });
2041
- if (options.captureGlobalErrors !== false)
2310
+ const onCrash = (error) => {
2311
+ if (recordedAsRejection(error)) return;
2312
+ captureException2(error, { "autter.unhandled": true });
2313
+ };
2314
+ let uninstallRejections = null;
2315
+ if (options.captureGlobalErrors !== false) {
2042
2316
  process.on("uncaughtExceptionMonitor", onCrash);
2317
+ if (options.captureUnhandledRejections !== false)
2318
+ uninstallRejections = installUnhandledRejectionCapture(
2319
+ (reason) => captureException2(reason, { "autter.unhandled_rejection": true })
2320
+ );
2321
+ }
2043
2322
  registerFlushTarget("logging", { forceFlush: () => flushRuntimeLogs() });
2044
2323
  let autoFlushHandle = options.autoFlush === false ? null : installAutterAutoFlush();
2045
2324
  const handle = {
@@ -2047,6 +2326,8 @@ function initAutterLogging(options) {
2047
2326
  flush: flushRuntimeLogs,
2048
2327
  shutdown: async () => {
2049
2328
  process.off("uncaughtExceptionMonitor", onCrash);
2329
+ uninstallRejections?.();
2330
+ uninstallRejections = null;
2050
2331
  setLoggerOnlyCapture(null);
2051
2332
  autoFlushHandle?.dispose();
2052
2333
  autoFlushHandle = null;
@@ -2152,11 +2433,16 @@ export {
2152
2433
  INGESTER_VERSION_HEADER,
2153
2434
  RuntimeError,
2154
2435
  SDK_IDENTITY,
2436
+ addBreadcrumb,
2437
+ autterErrorHandler,
2155
2438
  autterErrorResponse,
2156
2439
  autterFastify,
2440
+ autterFastifyErrors,
2441
+ autterKoa,
2157
2442
  autterRequests,
2158
2443
  captureException,
2159
2444
  captureMessage,
2445
+ captureRequestError,
2160
2446
  consoleSink,
2161
2447
  createBrowserRelayFetchHandler,
2162
2448
  createBrowserRelayHandler,
@@ -2191,6 +2477,7 @@ export {
2191
2477
  startCaughtExceptionSampler,
2192
2478
  toClientError,
2193
2479
  trackLlmCall,
2480
+ unhandledRejectionsMode,
2194
2481
  withLlmCall,
2195
2482
  withProcessSpan,
2196
2483
  withRuntimeOperation,