authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:40:08-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:40:08-07:00
logb6e384fb273b56ab9d12237f27684d1392be1692
tree983f753f23b23527f45869f4bec14c8f8b9e51f0
parentd8bf307d22663a3c458dc521a014fd21bdb60b98
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve trace IDs across error causes (#3426)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 54 insertions(+), 3 deletions(-)

packages/client-runtime/src/errors/errorTrace.test.ts+19
......@@ -1,3 +1,4 @@
1import * as Cause from "effect/Cause";
12import { describe, expect, it } from "vite-plus/test";
23
34import { findErrorTraceId } from "./errorTrace.ts";
......@@ -22,4 +23,22 @@ describe("findErrorTraceId", () => {
2223
2324 expect(findErrorTraceId(error)).toBeNull();
2425 });
26
27 it("finds trace metadata in Effect cause branches", () => {
28 const cause = Cause.fromReasons<unknown>([
29 Cause.makeFailReason(new Error("first failure")),
30 Cause.makeFailReason({ traceId: "trace-secondary" }),
31 ]);
32
33 expect(findErrorTraceId(cause)).toBe("trace-secondary");
34 });
35
36 it("finds trace metadata in aggregate error branches", () => {
37 const error = new AggregateError(
38 [new Error("first failure"), { traceId: "trace-aggregate" }],
39 "request failed",
40 );
41
42 expect(findErrorTraceId(error)).toBe("trace-aggregate");
43 });
2544});
packages/client-runtime/src/errors/errorTrace.ts+35-3
......@@ -1,17 +1,49 @@
1import * as Cause from "effect/Cause";
2
3const MAX_ERROR_TRACE_NODES = 128;
4
15export function findErrorTraceId(error: unknown): string | null {
26 const seen = new Set<object>();
3 let current: unknown = error;
7 const pending: Array<unknown> = [error];
8 let inspectedNodeCount = 0;
49
5 while (typeof current === "object" && current !== null && !seen.has(current)) {
10 while (pending.length > 0 && inspectedNodeCount < MAX_ERROR_TRACE_NODES) {
11 const current = pending.pop();
12 inspectedNodeCount += 1;
13 if (typeof current !== "object" || current === null || seen.has(current)) {
14 continue;
15 }
616 seen.add(current);
717 const record = current as {
818 readonly cause?: unknown;
19 readonly errors?: unknown;
920 readonly traceId?: unknown;
1021 };
1122 if (typeof record.traceId === "string" && record.traceId.trim().length > 0) {
1223 return record.traceId;
1324 }
14 current = record.cause;
25
26 if (Array.isArray(record.errors)) {
27 for (let index = record.errors.length - 1; index >= 0; index -= 1) {
28 pending.push(record.errors[index]);
29 }
30 }
31 if (Cause.isCause(current)) {
32 for (let index = current.reasons.length - 1; index >= 0; index -= 1) {
33 const reason = current.reasons[index];
34 switch (reason?._tag) {
35 case "Fail":
36 pending.push(reason.error);
37 break;
38 case "Die":
39 pending.push(reason.defect);
40 break;
41 }
42 }
43 }
44 if ("cause" in record) {
45 pending.push(record.cause);
46 }
1547 }
1648
1749 return null;