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 @@...@@ -1,3 +1,4 @@
1import * as Cause from "effect/Cause";
1import { describe, expect, it } from "vite-plus/test";2import { describe, expect, it } from "vite-plus/test";
23
3import { findErrorTraceId } from "./errorTrace.ts";4import { findErrorTraceId } from "./errorTrace.ts";
...@@ -22,4 +23,22 @@ describe("findErrorTraceId", () => {...@@ -22,4 +23,22 @@ describe("findErrorTraceId", () => {
2223
23 expect(findErrorTraceId(error)).toBeNull();24 expect(findErrorTraceId(error)).toBeNull();
24 });25 });
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 });
25});44});
packages/client-runtime/src/errors/errorTrace.ts+35-3
...@@ -1,17 +1,49 @@...@@ -1,17 +1,49 @@
1import * as Cause from "effect/Cause";
2
3const MAX_ERROR_TRACE_NODES = 128;
4
1export function findErrorTraceId(error: unknown): string | null {5export function findErrorTraceId(error: unknown): string | null {
2 const seen = new Set<object>();6 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 }
6 seen.add(current);16 seen.add(current);
7 const record = current as {17 const record = current as {
8 readonly cause?: unknown;18 readonly cause?: unknown;
19 readonly errors?: unknown;
9 readonly traceId?: unknown;20 readonly traceId?: unknown;
10 };21 };
11 if (typeof record.traceId === "string" && record.traceId.trim().length > 0) {22 if (typeof record.traceId === "string" && record.traceId.trim().length > 0) {
12 return record.traceId;23 return record.traceId;
13 }24 }
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 }
15 }47 }
1648
17 return null;49 return null;