| 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,9 +1,16 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; | ||
| 3 | import * as Option from "effect/Option"; | 4 | import * as Option from "effect/Option"; |
| 4 | import * as Tracer from "effect/Tracer"; | 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 | function collectingTracer(spans: Array<string>): Tracer.Tracer { | 15 | function collectingTracer(spans: Array<string>): Tracer.Tracer { |
| 9 | return Tracer.make({ | 16 | return Tracer.make({ |
| ... | @@ -54,4 +61,44 @@ describe("withRelayClientTracing", () => { | ... | @@ -54,4 +61,44 @@ describe("withRelayClientTracing", () => { |
| 54 | expect(userSpans).toEqual(["relay.operation"]); | 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,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 | const message = | 54 | const message = |
| 46 | value instanceof Error | 55 | value instanceof Error |
| 47 | ? value.message | 56 | ? value.message |
| ... | @@ -51,12 +60,19 @@ function traceSafeError(value: unknown): Error { | ... | @@ -51,12 +60,19 @@ function traceSafeError(value: unknown): Error { |
| 51 | typeof value.message === "string" | 60 | typeof value.message === "string" |
| 52 | ? value.message | 61 | ? value.message |
| 53 | : String(value); | 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 | if (value instanceof Error) { | 73 | if (value instanceof Error) { |
| 56 | error.name = value.name; | 74 | error.name = value.name; |
| 57 | if (value.stack !== undefined) { | 75 | error.stack = cleanTraceStack(value); |
| 58 | error.stack = value.stack; | ||
| 59 | } | ||
| 60 | } else if ( | 76 | } else if ( |
| 61 | typeof value === "object" && | 77 | typeof value === "object" && |
| 62 | value !== null && | 78 | value !== null && |
| ... | @@ -65,6 +81,9 @@ function traceSafeError(value: unknown): Error { | ... | @@ -65,6 +81,9 @@ function traceSafeError(value: unknown): Error { |
| 65 | ) { | 81 | ) { |
| 66 | error.name = value.name; | 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 | return error; | 87 | return error; |
| 69 | } | 88 | } |
| 70 | 89 |