| author | |
| committer | |
| log | ac77fe452bc5c69acbbd05b55029b26ec40d3b43 |
| tree | 83cf7b7618a84049aa2f85322074aad00d49f85f |
| parent | 4fbc4f9b20cc4d2e991349ce30b0118473de4621 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ |
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | |
| 3 | 4 | import * as Option from "effect/Option"; |
| 4 | 5 | import * as Tracer from "effect/Tracer"; |
| 6 | import { FetchHttpClient } from "effect/unstable/http"; | |
| 7 | import { vi } from "vite-plus/test"; | |
| 5 | 8 | |
| 6 | import { RelayClientTracer, withRelayClientTracing } from "./relayTracing.ts"; | |
| 9 | import { | |
| 10 | makeRelayClientTracingLayer, | |
| 11 | RelayClientTracer, | |
| 12 | withRelayClientTracing, | |
| 13 | } from "./relayTracing.ts"; | |
| 7 | 14 | |
| 8 | 15 | function collectingTracer(spans: Array<string>): Tracer.Tracer { |
| 9 | 16 | return Tracer.make({ |
| ... | ... | @@ -54,4 +61,44 @@ describe("withRelayClientTracing", () => { |
| 54 | 61 | expect(userSpans).toEqual(["relay.operation"]); |
| 55 | 62 | }), |
| 56 | 63 | ); |
| 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 | }); | |
| 57 | 104 | }); |
packages/shared/src/relayTracing.ts+24-5| ... | ... | @@ -41,7 +41,16 @@ export const withRelayClientTracing = <A, E, R>( |
| 41 | 41 | ), |
| 42 | 42 | ); |
| 43 | 43 | |
| 44 | function traceSafeError(value: unknown): Error { | |
| 44 | function 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 | ||
| 53 | function traceSafeError(value: unknown, seen = new WeakSet<object>()): Error { | |
| 45 | 54 | const message = |
| 46 | 55 | value instanceof Error |
| 47 | 56 | ? value.message |
| ... | ... | @@ -51,12 +60,19 @@ function traceSafeError(value: unknown): Error { |
| 51 | 60 | typeof value.message === "string" |
| 52 | 61 | ? value.message |
| 53 | 62 | : 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); | |
| 55 | 73 | if (value instanceof Error) { |
| 56 | 74 | error.name = value.name; |
| 57 | if (value.stack !== undefined) { | |
| 58 | error.stack = value.stack; | |
| 59 | } | |
| 75 | error.stack = cleanTraceStack(value); | |
| 60 | 76 | } else if ( |
| 61 | 77 | typeof value === "object" && |
| 62 | 78 | value !== null && |
| ... | ... | @@ -65,6 +81,9 @@ function traceSafeError(value: unknown): Error { |
| 65 | 81 | ) { |
| 66 | 82 | error.name = value.name; |
| 67 | 83 | } |
| 84 | if (cause) { | |
| 85 | error.stack = `${error.stack ?? `${error.name}: ${error.message}`}\nCaused by: ${cause.stack ?? `${cause.name}: ${cause.message}`}`; | |
| 86 | } | |
| 68 | 87 | return error; |
| 69 | 88 | } |
| 70 | 89 |