authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:07:06-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:07:06-07:00
logac77fe452bc5c69acbbd05b55029b26ec40d3b43
tree83cf7b7618a84049aa2f85322074aad00d49f85f
parent4fbc4f9b20cc4d2e991349ce30b0118473de4621
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve relay trace error causes (#3377)

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

2 files changed, 72 insertions(+), 6 deletions(-)

packages/shared/src/relayTracing.test.ts+48-1
......@@ -1,9 +1,16 @@
11import { describe, expect, it } from "@effect/vitest";
22import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as Option from "effect/Option";
45import * as Tracer from "effect/Tracer";
6import { FetchHttpClient } from "effect/unstable/http";
7import { vi } from "vite-plus/test";
58
6import { RelayClientTracer, withRelayClientTracing } from "./relayTracing.ts";
9import {
10 makeRelayClientTracingLayer,
11 RelayClientTracer,
12 withRelayClientTracing,
13} from "./relayTracing.ts";
714
815function collectingTracer(spans: Array<string>): Tracer.Tracer {
916 return Tracer.make({
......@@ -54,4 +61,44 @@ describe("withRelayClientTracing", () => {
5461 expect(userSpans).toEqual(["relay.operation"]);
5562 }),
5663 );
64
65 it.effect("preserves nested error causes in exported relay spans", () => {
66 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
67 const httpClientLayer = FetchHttpClient.layer.pipe(
68 Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)),
69 );
70 const tracingLayer = makeRelayClientTracingLayer(
71 {
72 tracesUrl: "https://api.axiom.test/v1/traces",
73 tracesDataset: "relay-traces",
74 tracesToken: "public-ingest-token",
75 },
76 {
77 serviceName: "relay-test",
78 runtime: "test",
79 client: "test",
80 },
81 ).pipe(Layer.provide(httpClientLayer));
82 const rootCause = new Error("relay socket closed");
83 const failure = new Error("relay request failed", { cause: rootCause });
84 const tracedApplication = Layer.effectDiscard(
85 Effect.fail(failure).pipe(
86 Effect.withSpan("relay.failed-operation"),
87 withRelayClientTracing,
88 Effect.exit,
89 ),
90 ).pipe(Layer.provide(tracingLayer));
91
92 return Layer.build(tracedApplication).pipe(
93 Effect.scoped,
94 Effect.andThen(
95 Effect.sync(() => {
96 expect(fetchFn).toHaveBeenCalledOnce();
97 const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);
98 expect(payload).toContain("relay request failed");
99 expect(payload).toContain("relay socket closed");
100 }),
101 ),
102 );
103 });
57104});
packages/shared/src/relayTracing.ts+24-5
......@@ -41,7 +41,16 @@ export const withRelayClientTracing = <A, E, R>(
4141 ),
4242 );
4343
44function traceSafeError(value: unknown): Error {
44function cleanTraceStack(error: Error): string {
45 const stack = error.stack ?? `${error.name}: ${error.message}`;
46 const lines = stack.split("\n");
47 const effectFrameIndex = lines.findIndex(
48 (line, index) => index > 0 && /(?:Generator\.next|~effect\/Effect)/.test(line),
49 );
50 return effectFrameIndex < 0 ? stack : lines.slice(0, effectFrameIndex).join("\n");
51}
52
53function traceSafeError(value: unknown, seen = new WeakSet<object>()): Error {
4554 const message =
4655 value instanceof Error
4756 ? value.message
......@@ -51,12 +60,19 @@ function traceSafeError(value: unknown): Error {
5160 typeof value.message === "string"
5261 ? value.message
5362 : String(value);
54 const error = new Error(message);
63
64 let cause: Error | undefined;
65 if (typeof value === "object" && value !== null && !seen.has(value)) {
66 seen.add(value);
67 if ("cause" in value && value.cause !== undefined) {
68 cause = traceSafeError(value.cause, seen);
69 }
70 }
71
72 const error = new Error(message, cause ? { cause } : undefined);
5573 if (value instanceof Error) {
5674 error.name = value.name;
57 if (value.stack !== undefined) {
58 error.stack = value.stack;
59 }
75 error.stack = cleanTraceStack(value);
6076 } else if (
6177 typeof value === "object" &&
6278 value !== null &&
......@@ -65,6 +81,9 @@ function traceSafeError(value: unknown): Error {
6581 ) {
6682 error.name = value.name;
6783 }
84 if (cause) {
85 error.stack = `${error.stack ?? `${error.name}: ${error.message}`}\nCaused by: ${cause.stack ?? `${cause.name}: ${cause.message}`}`;
86 }
6887 return error;
6988}
7089