// --------------------------------------------------------------------------- // The BROWSER export seam. The web client ships spans to /v1/traces, which the // worker forwards to the same Axiom dataset the worker's own spans land in, so // the browser must apply the same scrub — the redaction POLICY is covered by // `packages/core/sdk/src/span-redaction.test.ts`. // // This drives the real `OtlpTracer` (the layer `client.tsx` installs) with the // redacting serializer in front of a fake fetch, and asserts on the bytes that // would have gone over the wire. // --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; import { layerRedacted } from "./otlp-redaction"; // Synthetic placeholders only — never a real authorization code or token. const CODE = "synthetic-authorization-code"; const TOKEN = "synthetic-endpoint-token"; const EXPORT_URL = "https://app.test/v1/traces"; /** Runs `program` under a real `OtlpTracer` whose exporter posts through a fake * fetch, and returns every exported body. The exporter flushes from a scope * finalizer, so closing the scope drains the batch. */ const exportedBodies = ( program: Effect.Effect, serialization: Layer.Layer, ): Effect.Effect => Effect.gen(function* () { const decoder = new TextDecoder(); const bodies: string[] = []; const fetchLayer = Layer.succeed(FetchHttpClient.Fetch)((( _input: RequestInfo | URL, init?: RequestInit, ) => { // `OtlpSerialization.layerJson` produces an `HttpBody.Uint8Array`, so the // wire bytes arrive already encoded. const body = init?.body; bodies.push(body instanceof Uint8Array ? decoder.decode(body) : String(body ?? "")); return Promise.resolve(new Response(null, { status: 200 })); }) as typeof globalThis.fetch); yield* Effect.scoped( Effect.provide( program, OtlpTracer.layer({ url: EXPORT_URL, resource: { serviceName: "executor-web-test" }, exportInterval: "1 minute", }).pipe( Layer.provide(serialization), Layer.provide(FetchHttpClient.layer.pipe(Layer.provide(fetchLayer))), ), ), ); return bodies; }); /** One span carrying the attributes `HttpClient` stamps on every API request. */ const callbackSpan = Effect.void.pipe( Effect.withSpan("http.client GET", { attributes: { "url.full": `https://app.test/api/oauth/callback?code=${CODE}&domain=example.test`, "url.query": `code=${CODE}&domain=example.test`, "url.path": "/api/oauth/callback", }, }), ); describe("layerRedacted", () => { it.effect("scrubs url.full and url.query out of the exported payload", () => Effect.gen(function* () { const bodies = yield* exportedBodies( callbackSpan, layerRedacted(OtlpSerialization.layerJson), ); expect(bodies).toHaveLength(1); expect(bodies[0]).not.toContain(CODE); // Route-level visibility survives, and the stripped keys are recorded. expect(bodies[0]).toContain("/api/oauth/callback"); expect(bodies[0]).toContain("url.query.stripped_keys"); }), ); it.effect("the unwrapped serializer exports the code — the wrapper is load-bearing", () => Effect.gen(function* () { const bodies = yield* exportedBodies(callbackSpan, OtlpSerialization.layerJson); expect(bodies[0]).toContain(CODE); }), ); it.effect("scrubs a failed span's exception event and status message", () => Effect.gen(function* () { // A failing span: `OtlpTracer` renders the cause into an `exception` // event AND into `status.message`, neither of which is a URL attribute. const failing = Effect.fail( // oxlint-disable-next-line executor/no-error-constructor -- the failure text is what is under test; a tagged error would hide it new Error(`POST https://mcp.test/mcp?token=${TOKEN} failed with 401`), ).pipe(Effect.withSpan("http.client POST"), Effect.ignore); const bodies = yield* exportedBodies(failing, layerRedacted(OtlpSerialization.layerJson)); expect(bodies[0]).not.toContain(TOKEN); expect(bodies[0]).toContain("https://mcp.test/mcp"); expect(bodies[0]).toContain("failed with 401"); }), ); it.effect("leaves a span with nothing sensitive untouched", () => Effect.gen(function* () { const bodies = yield* exportedBodies( Effect.void.pipe( Effect.withSpan("http.client GET", { attributes: { "url.full": "https://app.test/api/integrations?owner=org" }, }), ), layerRedacted(OtlpSerialization.layerJson), ); expect(bodies[0]).toContain("https://app.test/api/integrations?owner=org"); expect(bodies[0]).not.toContain("url.query.stripped_keys"); }), ); });