@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/README.md CHANGED
@@ -48,6 +48,38 @@ expectOperation(runtime, "POST /checkout").toHaveOutcome("degraded").toHaveError
48
48
 
49
49
  Full guide: [Requests, coded errors and background work](../../docs/REQUESTS-AND-ERRORS.md).
50
50
 
51
+ # Breadcrumbs (1.6.0)
52
+
53
+ The steps before an error, scoped to the current request:
54
+
55
+ ```ts
56
+ import { addBreadcrumb, runtimeLogger } from "@autter/runtime-node";
57
+
58
+ app.post("/checkout", async (req, res) => {
59
+ addBreadcrumb({ message: "Loaded cart", data: { items: 3 } }); // type "custom"
60
+ runtimeLogger.info("Charging card", { provider: "stripe" }); // recorded as a "log" breadcrumb
61
+ await charge(); // a throw here carries both
62
+ });
63
+ ```
64
+
65
+ - Scope: inside `autterRequests()` / `withRuntimeRequest()` /
66
+ `withRuntimeOperation()` the trail belongs to the request and is shared with
67
+ its child operations and forks (AsyncLocalStorage). Inside a traced request
68
+ without the middleware it belongs to the trace; outside both, to the process
69
+ (so a crash outside any request carries what happened before it).
70
+ - The last 30 are kept. `captureException`, `captureMessage` and errors thrown
71
+ out of operations attach them to the exception span event as
72
+ `autter.breadcrumbs` (a JSON string, at most 16 KB, or less when
73
+ `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` is lower).
74
+ - Every `runtimeLogger` / `runtimeContext.debug|info|warn|error` entry is
75
+ recorded as a `log` breadcrumb with its level and up to 10 primitive
76
+ attributes.
77
+ - Messages (≤ 500 characters) and data (≤ 10 keys of strings, numbers,
78
+ booleans or null) are scrubbed with your `redact` settings.
79
+ - The browser relay forwards browser breadcrumbs, re-validated and re-scrubbed.
80
+ - Needs ingester 1.6.0. Logger-only mode (`initAutterLogging`) and
81
+ `@autter/runtime-edge` don't attach them yet.
82
+
51
83
  # @autter/runtime-node
52
84
 
53
85
  The server tracker exports per-instance RSS, heap, memory limit, and GC
@@ -113,6 +145,12 @@ const autter = initAutterServer({
113
145
  // handled errors — always recorded, never sampled out:
114
146
  autter.captureException(err, { "order.id": "…" });
115
147
 
148
+ // fingerprint override — every error carrying the same fingerprint is one
149
+ // issue, whatever its message or stack (a string or up to 10 strings, each
150
+ // ≤200 chars; sent as `autter.fingerprint` on the exception event; needs
151
+ // ingester 1.6.0+, older ingesters ignore it):
152
+ autter.captureException(err, { fingerprint: ["checkout", "card-timeout"] });
153
+
116
154
  // warnings/info without an exception — same grouping+aggregation as
117
155
  // errors, just a lower severity ("fatal" | "error" | "warning" | "info"):
118
156
  autter.captureMessage("Legacy /orders lookup used", "warning");
@@ -142,7 +180,7 @@ Defaults (cheap by construction):
142
180
  | Traces containing an error | 100% (tail retention, `retainTracesOnError`) |
143
181
  | `withProcessSpan` spans | 100% (same always-on tracer) |
144
182
  | LLM/GenAI call spans | 100% (`llmTracing`, on by default) |
145
- | Healthy traces | 1% head sampling (`traceSampleRate`) |
183
+ | Healthy traces | 1% head sampling (`traceSampleRate`; also for requests whose incoming `traceparent` is unsampled, such as browser fetches from `@autter/runtime-browser`) |
146
184
  | Request metrics | exported every 60 s |
147
185
  | Logs | not collected |
148
186
  | PII in custom attributes | redacted before export (`redactAttributes`) |
@@ -167,6 +205,63 @@ import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
167
205
  initAutterServer({ ..., instrumentations: [new ExpressInstrumentation()] });
168
206
  ```
169
207
 
208
+ ### Server errors keep their stacks
209
+
210
+ When a handler throws, the framework answers 500 — and on its own that 500
211
+ is all the tracker sees: a stackless `HttpServerError` grouped by route. Add
212
+ your framework's hook and the thrown error is recorded, with its stack, on
213
+ the request span; the framework then carries on exactly as before.
214
+
215
+ ```ts
216
+ import { autterErrorHandler, autterFastifyErrors, autterKoa, captureRequestError } from "@autter/runtime-node";
217
+
218
+ // Express / Connect: after your routes, before your own error handlers.
219
+ // Records the error, then calls next(err) unchanged.
220
+ app.use(autterErrorHandler());
221
+
222
+ // Fastify 4/5: an onError hook (Fastify's error handler still answers).
223
+ // autterFastify (request summaries) already includes it.
224
+ app.register(autterFastifyErrors); // or autterFastifyErrors(app)
225
+
226
+ // Koa 2/3: mount first; records and rethrows, so app.on("error") still fires.
227
+ app.use(autterKoa());
228
+
229
+ // Anything else (NestJS exception filters, hapi onPreResponse, …):
230
+ captureRequestError(err, { "app.handler": "checkout" });
231
+ ```
232
+
233
+ All three report errors with status ≥ 500 (or none), RuntimeErrors and coded
234
+ errors — `ctx.throw(404)` and validation failures stay out of your issues.
235
+ Pass `{ capture: true }` to report every error, or a predicate. An error is
236
+ reported once, even if your handler already called `captureException`, and it
237
+ lands on the request's HTTP server span even when an instrumentation's child
238
+ span is active (`captureRequestError` still adds its attributes to that span
239
+ when the error was already reported). Errors from another JavaScript realm
240
+ (a `vm` context, Next's edge sandbox under `next start`) keep their own type,
241
+ message and stack. `autterErrorResponse()` reports the same way and also
242
+ answers the request.
243
+
244
+ ### Unhandled promise rejections
245
+
246
+ Recorded as errors (severity `error`, `autter.unhandled_rejection: true`, not
247
+ fatal) — without changing whether your process crashes. Any
248
+ `unhandledRejection` listener switches off Node's default crash, so the SDK
249
+ does what Node would have done when its listener is the only one:
250
+
251
+ | `--unhandled-rejections` | Your own listener? | What happens | Recorded as |
252
+ | --- | --- | --- | --- |
253
+ | `throw` (default) | no | rethrown on the next tick: exit code 1, as before | fatal crash (`autter.unhandled`) |
254
+ | `throw` (default) | yes | your listener decides | error |
255
+ | `strict` | either | Node crashes first, as before | fatal crash |
256
+ | `warn`, `none` | either | no crash, as before | error |
257
+ | `warn-with-error-code` | no | Node's warning and exit code 1, as before | error |
258
+
259
+ The one visible difference: after the rethrow, `uncaughtException` listeners
260
+ receive origin `"uncaughtException"` rather than `"unhandledRejection"`.
261
+ `captureUnhandledRejections: false` (or `captureGlobalErrors: false`) restores
262
+ the 1.5.0 behaviour, where a rejection is seen only when it crashes the
263
+ process.
264
+
170
265
  ### Version compatibility: checked for you
171
266
 
172
267
  Some features need a minimum ingester version: operation logging needs
@@ -90,6 +90,35 @@ var compat_manifest_default = {
90
90
  ingester: "1.5.0",
91
91
  migrations: ["0013-occurrence-codes"],
92
92
  sdks: { "@autter/runtime-node": "1.5.0", "@autter/runtime-next": "1.5.0", "@autter/runtime-browser": "1.4.0", "@autter/runtime-edge": "1.0.0" }
93
+ },
94
+ {
95
+ id: "breadcrumbs",
96
+ title: "Breadcrumbs: the steps before an error",
97
+ ingester: "1.6.0",
98
+ migrations: [],
99
+ sdks: { "@autter/runtime-browser": "1.6.0", "@autter/runtime-node": "1.6.0", "@autter/runtime-next": "1.6.0" }
100
+ },
101
+ {
102
+ id: "source_map_debug_ids",
103
+ title: "Source map debug ids",
104
+ ingester: "1.6.0",
105
+ migrations: ["0017-source-maps-debug-id"],
106
+ route: "/v1/sourcemaps/check",
107
+ sdks: { "@autter/runtime-browser": "1.6.0", "@autter/runtime-next": "1.6.0" }
108
+ },
109
+ {
110
+ id: "fingerprint_override",
111
+ title: "Fingerprint override",
112
+ ingester: "1.6.0",
113
+ migrations: [],
114
+ sdks: { "@autter/runtime-browser": "1.6.0", "@autter/runtime-node": "1.6.0", "@autter/runtime-next": "1.6.0", "@autter/runtime-edge": "1.1.0" }
115
+ },
116
+ {
117
+ id: "browser_trace_propagation",
118
+ title: "Browser trace propagation (browser errors linked to server traces)",
119
+ ingester: "1.6.0",
120
+ migrations: [],
121
+ sdks: { "@autter/runtime-browser": "1.6.0", "@autter/runtime-next": "1.6.0", "@autter/runtime-node": "1.6.0" }
93
122
  }
94
123
  ]
95
124
  };
@@ -435,7 +464,7 @@ var _https = require('https');
435
464
  // package.json
436
465
  var package_default = {
437
466
  name: "@autter/runtime-node",
438
- version: "1.5.0",
467
+ version: "1.6.0",
439
468
  description: "Autter Runtime for Node.js: request wide events, coded errors, operation logging, same-origin browser relay and a curated OpenTelemetry server tracker",
440
469
  license: "MIT",
441
470
  type: "module",
@@ -486,6 +515,8 @@ var package_default = {
486
515
  "@autter/runtime-core": "0.0.0",
487
516
  "@types/node": "^22.10.0",
488
517
  express: "^4.21.2",
518
+ fastify: "^5.12.5",
519
+ koa: "^3.2.1",
489
520
  tsup: "^8.3.5",
490
521
  typescript: "^5.7.0"
491
522
  },
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  noteCompatFeature,
3
3
  observeIngesterResponse
4
- } from "./chunk-L7HWKMCZ.js";
4
+ } from "./chunk-OY2HTUWY.js";
5
5
 
6
6
  // ../runtime-core/dist/index.js
7
7
  var SENSITIVE_KEY_PATTERNS = [
@@ -327,8 +327,8 @@ function priority(key) {
327
327
  if (/^(?:exception|autter\.error)\./.test(key)) return 1;
328
328
  return 0;
329
329
  }
330
- function boundContext(attributes = {}, redact2) {
331
- const redacted = redact2 ? redact2(attributes) : redactAttributes(attributes);
330
+ function boundContext(attributes = {}, redact3) {
331
+ const redacted = redact3 ? redact3(attributes) : redactAttributes(attributes);
332
332
  let budget = 512;
333
333
  let characters = 16384;
334
334
  let truncated = false;
@@ -365,14 +365,14 @@ function boundContext(attributes = {}, redact2) {
365
365
  if (truncated) result["autter.context.truncated"] = true;
366
366
  return result;
367
367
  }
368
- function userContext(attributes = {}, redact2) {
368
+ function userContext(attributes = {}, redact3) {
369
369
  return Object.fromEntries(
370
- Object.entries(boundContext(attributes, redact2)).filter(
370
+ Object.entries(boundContext(attributes, redact3)).filter(
371
371
  ([key]) => !RESERVED_KEY.test(key)
372
372
  )
373
373
  );
374
374
  }
375
- function mergeContext(current, next = {}, redact2) {
375
+ function mergeContext(current, next = {}, redact3) {
376
376
  const merge2 = (left, right, depth) => {
377
377
  const result = { ...left };
378
378
  for (const [key, value] of Object.entries(right)) {
@@ -381,7 +381,7 @@ function mergeContext(current, next = {}, redact2) {
381
381
  }
382
382
  return result;
383
383
  };
384
- return userContext(merge2(current, userContext(next, redact2), 0), redact2);
384
+ return userContext(merge2(current, userContext(next, redact3), 0), redact3);
385
385
  }
386
386
  function toSpanAttributes(context) {
387
387
  return Object.fromEntries(
@@ -473,6 +473,17 @@ function defineRuntimeErrors(namespace, catalog) {
473
473
  }
474
474
  return out;
475
475
  }
476
+ function isError(value) {
477
+ if (!value || typeof value !== "object") return false;
478
+ try {
479
+ if (value instanceof Error) return true;
480
+ const native = Error.isError;
481
+ if (typeof native === "function") return native(value) === true;
482
+ return Object.prototype.toString.call(value) === "[object Error]";
483
+ } catch {
484
+ return false;
485
+ }
486
+ }
476
487
  function isRuntimeErrorLike(err) {
477
488
  if (err instanceof RuntimeError) return true;
478
489
  if (!err || typeof err !== "object") return false;
@@ -523,15 +534,15 @@ function errorAttributes(err, options = {}) {
523
534
  for (let n = 1; n <= CAUSE_DEPTH && cause !== void 0 && cause !== null; n++) {
524
535
  if (seen.has(cause)) break;
525
536
  seen.add(cause);
526
- const isError = typeof cause === "object";
527
- const type = isError ? String(field(cause, "name") ?? cause.constructor?.name ?? "Error") : typeof cause;
528
- const message = isError ? field(cause, "message") : cause;
537
+ const isError2 = typeof cause === "object";
538
+ const type = isError2 ? String(field(cause, "name") ?? cause.constructor?.name ?? "Error") : typeof cause;
539
+ const message = isError2 ? field(cause, "message") : cause;
529
540
  out[`exception.cause.${n}.type`] = type.slice(0, 200);
530
541
  out[`exception.cause.${n}.message`] = String(message ?? "").slice(0, 1e3);
531
542
  const causeCode = field(cause, "code");
532
543
  if (typeof causeCode === "string" && causeCode)
533
544
  out[`exception.cause.${n}.code`] = causeCode.slice(0, CODE_MAX_LENGTH);
534
- cause = isError ? field(cause, "cause") : void 0;
545
+ cause = isError2 ? field(cause, "cause") : void 0;
535
546
  }
536
547
  }
537
548
  return out;
@@ -577,19 +588,19 @@ function createInlineLogState(startedAt) {
577
588
  level: "info"
578
589
  };
579
590
  }
580
- function appendInlineLog(state, level, message, attrs, redact2, now = Date.now()) {
591
+ function appendInlineLog(state, level, message, attrs, redact3, now = Date.now()) {
581
592
  state.level = maxLevel(state.level, level);
582
593
  if (state.logs.length >= INLINE_LOG_LIMIT) {
583
594
  state.logsTruncated = true;
584
595
  return;
585
596
  }
586
- const text = String(boundContext({ message }, redact2).message ?? "").slice(
597
+ const text = String(boundContext({ message }, redact3).message ?? "").slice(
587
598
  0,
588
599
  INLINE_MESSAGE_MAX
589
600
  );
590
601
  let small;
591
602
  if (attrs && Object.keys(attrs).length) {
592
- small = boundContext(attrs, redact2);
603
+ small = boundContext(attrs, redact3);
593
604
  const encoded = JSON.stringify(small);
594
605
  if (encoded.length > 1e3) small = { "autter.context.truncated": true };
595
606
  }
@@ -806,6 +817,172 @@ function parseUserAgent(ua) {
806
817
  const device = !value ? "unknown" : bot ? "bot" : /iPad|Tablet/.test(value) || /Android/.test(value) && !/Mobile/.test(value) ? "tablet" : /Mobi|iPhone|iPod|Android/.test(value) ? "mobile" : "desktop";
807
818
  return { browser, os, device, bot };
808
819
  }
820
+ var BREADCRUMBS_ATTRIBUTE = "autter.breadcrumbs";
821
+ var MAX_BREADCRUMBS = 30;
822
+ var MAX_MESSAGE = 500;
823
+ var MAX_DATA_KEYS = 10;
824
+ var MAX_DATA_STRING = 200;
825
+ var BREADCRUMBS_ATTRIBUTE_MAX_CHARS = 16 * 1024;
826
+ var MASK = "[redacted]";
827
+ var TYPES = /* @__PURE__ */ new Set(["console", "request", "navigation", "click", "log", "custom"]);
828
+ var LEVELS = {
829
+ debug: "debug",
830
+ trace: "debug",
831
+ info: "info",
832
+ log: "info",
833
+ warning: "warning",
834
+ warn: "warning",
835
+ error: "error",
836
+ fatal: "error"
837
+ };
838
+ var UNSAFE_KEY = /^(?:__proto__|constructor|prototype)$/;
839
+ function scrubber(redact3) {
840
+ return (value, max) => {
841
+ const scrubbed = redact3({ message: value.slice(0, max + 512) }).message;
842
+ return stripUrlQueries(typeof scrubbed === "string" ? scrubbed : "").slice(0, max);
843
+ };
844
+ }
845
+ function cleanData(raw, redact3) {
846
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
847
+ const picked = {};
848
+ let count = 0;
849
+ for (const key of Object.keys(raw).slice(0, 64)) {
850
+ if (count >= MAX_DATA_KEYS) break;
851
+ if (!key || UNSAFE_KEY.test(key)) continue;
852
+ const value = raw[key];
853
+ const name = key.slice(0, 100);
854
+ if (typeof value === "string") {
855
+ const text = /(?:url|path|route|target)$/i.test(name) ? value.split(/[?#]/)[0] : value;
856
+ picked[name] = text.slice(0, MAX_DATA_STRING + 512);
857
+ } else if (typeof value === "number") {
858
+ if (!Number.isFinite(value)) continue;
859
+ picked[name] = value;
860
+ } else if (typeof value === "boolean" || value === null) {
861
+ picked[name] = value;
862
+ } else {
863
+ continue;
864
+ }
865
+ count++;
866
+ }
867
+ if (count === 0) return void 0;
868
+ const redacted = redact3(picked);
869
+ const out = {};
870
+ for (const key of Object.keys(picked)) {
871
+ const value = key in redacted ? redacted[key] : picked[key];
872
+ out[key] = typeof value === "string" ? stripUrlQueries(value).slice(0, MAX_DATA_STRING) : value;
873
+ }
874
+ return out;
875
+ }
876
+ function timestamp(value) {
877
+ if (value === void 0) return (/* @__PURE__ */ new Date()).toISOString();
878
+ if (typeof value !== "string" && typeof value !== "number" && !(value instanceof Date)) return null;
879
+ const at = new Date(value);
880
+ return Number.isNaN(at.getTime()) ? null : at.toISOString();
881
+ }
882
+ function normalizeBreadcrumb(input, redact3, options = {}) {
883
+ try {
884
+ if (!input || typeof input !== "object" || Array.isArray(input)) return null;
885
+ const raw = input;
886
+ if (options.requireAt && raw.at === void 0) return null;
887
+ const at = timestamp(raw.at);
888
+ if (!at) return null;
889
+ const rawMessage = raw.message;
890
+ const message = typeof rawMessage === "string" ? rawMessage : typeof rawMessage === "number" || typeof rawMessage === "boolean" ? String(rawMessage) : "";
891
+ const data = cleanData(raw.data, redact3);
892
+ if (!message && !data) return null;
893
+ const type = typeof raw.type === "string" && TYPES.has(raw.type) ? raw.type : "custom";
894
+ const level = typeof raw.level === "string" ? LEVELS[raw.level.toLowerCase()] : void 0;
895
+ return {
896
+ type,
897
+ at,
898
+ ...level ? { level } : {},
899
+ message: scrubber(redact3)(message, MAX_MESSAGE),
900
+ ...data ? { data } : {}
901
+ };
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+ function sanitizeBreadcrumbs(raw, redact3) {
907
+ const out = [];
908
+ try {
909
+ if (!Array.isArray(raw)) return void 0;
910
+ const first = Math.max(0, raw.length - 200);
911
+ for (let i = raw.length - 1; i >= first && out.length < MAX_BREADCRUMBS; i--) {
912
+ const crumb = normalizeBreadcrumb(raw[i], redact3, { requireAt: true });
913
+ if (crumb) out.push(crumb);
914
+ }
915
+ } catch {
916
+ }
917
+ return out.length ? out.reverse() : void 0;
918
+ }
919
+ var BreadcrumbTrail = class {
920
+ constructor(max = MAX_BREADCRUMBS) {
921
+ this.max = max;
922
+ }
923
+ max;
924
+ items = [];
925
+ add(crumb) {
926
+ this.items.push(crumb);
927
+ if (this.items.length > this.max) this.items.splice(0, this.items.length - this.max);
928
+ }
929
+ /** Copies, oldest first. */
930
+ list() {
931
+ return this.items.map((crumb) => ({ ...crumb, ...crumb.data ? { data: { ...crumb.data } } : {} }));
932
+ }
933
+ get size() {
934
+ return this.items.length;
935
+ }
936
+ clear() {
937
+ this.items = [];
938
+ }
939
+ };
940
+ function breadcrumbsAttribute(list, scrub, maxChars = BREADCRUMBS_ATTRIBUTE_MAX_CHARS) {
941
+ const parts = [];
942
+ let chars = 2;
943
+ for (let i = list.length - 1; i >= 0 && parts.length < MAX_BREADCRUMBS; i--) {
944
+ const crumb = list[i];
945
+ let json = JSON.stringify(crumb);
946
+ if (scrub(json) !== json) {
947
+ json = JSON.stringify({ type: crumb.type, at: crumb.at, ...crumb.level ? { level: crumb.level } : {}, message: MASK });
948
+ }
949
+ if (chars + json.length + 1 > maxChars) break;
950
+ chars += json.length + 1;
951
+ parts.push(json);
952
+ }
953
+ if (parts.length === 0) return void 0;
954
+ const out = `[${parts.reverse().join(",")}]`;
955
+ return scrub(out) === out ? out : void 0;
956
+ }
957
+ var FINGERPRINT_ATTRIBUTE = "autter.fingerprint";
958
+ var FINGERPRINT_MAX_VALUES = 10;
959
+ var FINGERPRINT_MAX_LENGTH = 200;
960
+ function fingerprintValues(value, scrub = (text) => text) {
961
+ const list = Array.isArray(value) ? value : [value];
962
+ const values = list.filter((item) => typeof item === "string" && item !== "").slice(0, FINGERPRINT_MAX_VALUES).map((item) => scrub(item.slice(0, FINGERPRINT_MAX_LENGTH + 512)).slice(0, FINGERPRINT_MAX_LENGTH)).filter((item) => item !== "");
963
+ return values.length > 0 ? values : void 0;
964
+ }
965
+ function fingerprintEventAttributes(value, scrub) {
966
+ const values = fingerprintValues(value, scrub);
967
+ return values ? { [FINGERPRINT_ATTRIBUTE]: JSON.stringify(values) } : {};
968
+ }
969
+ function splitFingerprint(attributes) {
970
+ if (!attributes || typeof attributes !== "object" || !("fingerprint" in attributes)) {
971
+ return { attributes: attributes ?? void 0, fingerprint: void 0 };
972
+ }
973
+ const { fingerprint, ...rest } = attributes;
974
+ return { attributes: rest, fingerprint };
975
+ }
976
+
977
+ // src/breadcrumbs.ts
978
+ import { trace as trace2 } from "@opentelemetry/api";
979
+
980
+ // src/logger.ts
981
+ import { AsyncLocalStorage } from "async_hooks";
982
+ import { randomUUID } from "crypto";
983
+ import {
984
+ trace
985
+ } from "@opentelemetry/api";
809
986
 
810
987
  // src/sinks.ts
811
988
  import {
@@ -1185,11 +1362,6 @@ function memorySink() {
1185
1362
  }
1186
1363
 
1187
1364
  // src/logger.ts
1188
- import { AsyncLocalStorage } from "async_hooks";
1189
- import { randomUUID } from "crypto";
1190
- import {
1191
- trace
1192
- } from "@opentelemetry/api";
1193
1365
  var local = new AsyncLocalStorage();
1194
1366
  var config = null;
1195
1367
  var sinks = [];
@@ -1237,6 +1409,7 @@ function emit(level, message, attributes, extra = {}, options = {}) {
1237
1409
  const state = options.state !== void 0 ? options.state ?? void 0 : local.getStore();
1238
1410
  if (state?.sealed) return;
1239
1411
  const summary = extra["autter.event.type"] === "operation";
1412
+ if (!summary) recordLogBreadcrumb(level, message, attributes, state);
1240
1413
  if (!summary && LEVEL_SEVERITY[level] < LEVEL_SEVERITY[config?.options?.minLevel ?? "debug"])
1241
1414
  return;
1242
1415
  if (!summary && state && config?.options?.inline !== false) {
@@ -1303,6 +1476,9 @@ function configureRuntimeLogger(next) {
1303
1476
  }
1304
1477
  }
1305
1478
  }
1479
+ function loggerConfigured() {
1480
+ return config;
1481
+ }
1306
1482
  function addRuntimeSink(sink) {
1307
1483
  extraSinks.add(sink);
1308
1484
  return () => extraSinks.delete(sink);
@@ -1313,8 +1489,8 @@ function notifyException(error, attributes = {}) {
1313
1489
  const record = {
1314
1490
  time: Date.now(),
1315
1491
  error,
1316
- message: error instanceof Error ? error.message : String(error),
1317
- type: error instanceof Error ? error.name : "Error",
1492
+ message: isError(error) ? error.message : String(error),
1493
+ type: isError(error) ? error.name : "Error",
1318
1494
  attributes: { ...attributes, ...errorAttributes(error) },
1319
1495
  ...state ? { operationId: state.id } : {},
1320
1496
  ...state?.requestId ? { requestId: state.requestId } : {}
@@ -1367,11 +1543,11 @@ function createRuntimeLogger(attributes = {}) {
1367
1543
  warn: (message, extra) => emit("warning", message, merge(attrs, extra)),
1368
1544
  error: (error, extra) => emit(
1369
1545
  "error",
1370
- error instanceof Error ? error.message : String(error),
1546
+ isError(error) ? error.message : String(error),
1371
1547
  {
1372
1548
  ...attrs,
1373
1549
  ...extra,
1374
- ...error instanceof Error ? {
1550
+ ...isError(error) ? {
1375
1551
  "exception.type": error.name,
1376
1552
  "exception.stacktrace": error.stack ?? ""
1377
1553
  } : {}
@@ -1382,16 +1558,16 @@ function createRuntimeLogger(attributes = {}) {
1382
1558
  }
1383
1559
  var runtimeLogger = createRuntimeLogger();
1384
1560
  function captureExceptionAsLog(error, attributes = {}) {
1385
- const isError = error instanceof Error;
1386
- const message = isError ? error.message : String(error);
1387
- const stack = isError && error.stack ? error.stack : new Error().stack?.split("\n").filter((line, i) => i === 0 || !/captureException/.test(line)).join("\n") ?? "";
1561
+ const isErr = isError(error);
1562
+ const message = isErr ? error.message : String(error);
1563
+ const stack = isErr && error.stack ? error.stack : new Error().stack?.split("\n").filter((line, i) => i === 0 || !/captureException/.test(line)).join("\n") ?? "";
1388
1564
  noteOperationError(error);
1389
1565
  emit(
1390
1566
  "error",
1391
1567
  message,
1392
1568
  {
1393
1569
  ...attributes,
1394
- "exception.type": isError ? error.name : "Error",
1570
+ "exception.type": isErr ? error.name : "Error",
1395
1571
  "exception.message": message,
1396
1572
  "exception.stacktrace": stack
1397
1573
  },
@@ -1486,7 +1662,7 @@ function emitSummary(state, options = {}) {
1486
1662
  state.message ?? `${state.name}: ${state.outcome}`,
1487
1663
  {
1488
1664
  ...operationAttributes(state),
1489
- ...err instanceof Error ? {
1665
+ ...isError(err) ? {
1490
1666
  "exception.type": err.name,
1491
1667
  "exception.stacktrace": err.stack ?? ""
1492
1668
  } : {}
@@ -1521,6 +1697,7 @@ function startOperation(name, fn, attributes, options, internal = {}) {
1521
1697
  ...parent?.requestId ? { requestId: parent.requestId } : {}
1522
1698
  }
1523
1699
  );
1700
+ if (linked && parent) state.breadcrumbs = parent.breadcrumbs ??= new BreadcrumbTrail();
1524
1701
  const operation = operationHandle(state);
1525
1702
  const run = () => local.run(state, async () => {
1526
1703
  let thrown;
@@ -1530,6 +1707,7 @@ function startOperation(name, fn, attributes, options, internal = {}) {
1530
1707
  state.outcome = "failed";
1531
1708
  thrown = error;
1532
1709
  noteOperationError(error, true, state);
1710
+ rememberThrownBreadcrumbs(error, state);
1533
1711
  config?.captureThrown?.(error);
1534
1712
  throw error;
1535
1713
  } finally {
@@ -1634,6 +1812,83 @@ var runtimeContext = {
1634
1812
  }
1635
1813
  };
1636
1814
 
1815
+ // src/breadcrumbs.ts
1816
+ var INVALID_TRACE_ID = "00000000000000000000000000000000";
1817
+ var MAX_TRACE_TRAILS = 200;
1818
+ var byTrace = /* @__PURE__ */ new Map();
1819
+ var processTrail = new BreadcrumbTrail();
1820
+ var thrownTrails = /* @__PURE__ */ new WeakMap();
1821
+ function redact2(attributes) {
1822
+ return loggerConfigured()?.redact(attributes) ?? redactAttributes(attributes);
1823
+ }
1824
+ function trailFor(create, state = local.getStore()) {
1825
+ if (state) {
1826
+ if (!state.breadcrumbs && create) state.breadcrumbs = new BreadcrumbTrail();
1827
+ return state.breadcrumbs;
1828
+ }
1829
+ const traceId = trace2.getActiveSpan()?.spanContext().traceId;
1830
+ if (traceId && traceId !== INVALID_TRACE_ID) {
1831
+ let trail = byTrace.get(traceId);
1832
+ if (!trail && create) {
1833
+ trail = new BreadcrumbTrail();
1834
+ byTrace.set(traceId, trail);
1835
+ if (byTrace.size > MAX_TRACE_TRAILS) {
1836
+ const oldest = byTrace.keys().next().value;
1837
+ if (oldest !== void 0) byTrace.delete(oldest);
1838
+ }
1839
+ }
1840
+ return trail;
1841
+ }
1842
+ return processTrail;
1843
+ }
1844
+ function addBreadcrumb(input) {
1845
+ try {
1846
+ const crumb = normalizeBreadcrumb(input, redact2);
1847
+ if (!crumb) return;
1848
+ trailFor(true)?.add(crumb);
1849
+ noteCompatFeature("breadcrumbs");
1850
+ } catch {
1851
+ }
1852
+ }
1853
+ function recordLogBreadcrumb(level, message, attributes, state) {
1854
+ try {
1855
+ const data = {};
1856
+ let count = 0;
1857
+ for (const [key, value] of Object.entries(attributes)) {
1858
+ if (count >= 10) break;
1859
+ if (/^(?:exception|autter)\./.test(key)) continue;
1860
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
1861
+ data[key] = value;
1862
+ count++;
1863
+ }
1864
+ }
1865
+ const crumb = normalizeBreadcrumb({ type: "log", level, message, data }, redact2);
1866
+ if (crumb) trailFor(true, state)?.add(crumb);
1867
+ } catch {
1868
+ }
1869
+ }
1870
+ function attributeBudget() {
1871
+ const limit = Number(
1872
+ process.env.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT ?? process.env.OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT
1873
+ );
1874
+ return Number.isFinite(limit) && limit > 0 ? Math.min(BREADCRUMBS_ATTRIBUTE_MAX_CHARS, limit) : BREADCRUMBS_ATTRIBUTE_MAX_CHARS;
1875
+ }
1876
+ function rememberThrownBreadcrumbs(error, state) {
1877
+ if (state.breadcrumbs && error !== null && (typeof error === "object" || typeof error === "function"))
1878
+ thrownTrails.set(error, state.breadcrumbs);
1879
+ }
1880
+ function breadcrumbEventAttributes(scrub, error) {
1881
+ try {
1882
+ const thrown = error !== null && typeof error === "object" ? thrownTrails.get(error) : void 0;
1883
+ const trail = thrown ?? trailFor(false);
1884
+ if (!trail?.size) return {};
1885
+ const json = breadcrumbsAttribute(trail.list(), scrub, attributeBudget());
1886
+ return json ? { [BREADCRUMBS_ATTRIBUTE]: json } : {};
1887
+ } catch {
1888
+ return {};
1889
+ }
1890
+ }
1891
+
1637
1892
  export {
1638
1893
  redactAttributes,
1639
1894
  redactText,
@@ -1641,6 +1896,7 @@ export {
1641
1896
  CODE_PATTERN,
1642
1897
  RuntimeError,
1643
1898
  defineRuntimeErrors,
1899
+ isError,
1644
1900
  isRuntimeErrorLike,
1645
1901
  errorCodeOf,
1646
1902
  errorAttributes,
@@ -1654,6 +1910,12 @@ export {
1654
1910
  compileIgnore,
1655
1911
  requestOutcome,
1656
1912
  parseUserAgent,
1913
+ sanitizeBreadcrumbs,
1914
+ fingerprintValues,
1915
+ fingerprintEventAttributes,
1916
+ splitFingerprint,
1917
+ addBreadcrumb,
1918
+ breadcrumbEventAttributes,
1657
1919
  otlpSink,
1658
1920
  consoleSink,
1659
1921
  fileSink,