feat(core-testing): RecordingTracer for span assertions
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { RecordingTracer } from "@/instrumentation/recording-tracer";
|
||||
|
||||
describe("RecordingTracer", () => {
|
||||
it("records every startSpan call with name, op, attributes, status, durationMs", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
await tracer.startSpan(
|
||||
{ name: "blog.getArticles", op: "use-case", attributes: { limit: 10 } },
|
||||
async (span) => {
|
||||
span.setAttribute("count", 3);
|
||||
span.setStatus("ok");
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
expect(tracer.spans).toHaveLength(1);
|
||||
const s = tracer.spans[0]!;
|
||||
expect(s.name).toBe("blog.getArticles");
|
||||
expect(s.op).toBe("use-case");
|
||||
expect(s.attributes).toMatchObject({ limit: 10, count: 3 });
|
||||
expect(s.status).toBe("ok");
|
||||
expect(typeof s.durationMs).toBe("number");
|
||||
expect(s.durationMs).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("records error status when fn throws", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
await expect(
|
||||
tracer.startSpan({ name: "x" }, async () => {
|
||||
throw new Error("boom");
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
expect(tracer.spans).toHaveLength(1);
|
||||
expect(tracer.spans[0]!.status).toBe("error");
|
||||
expect(tracer.spans[0]!.statusMessage).toBe("boom");
|
||||
});
|
||||
|
||||
it("records error status when set explicitly via span.setStatus", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
await tracer.startSpan({ name: "x" }, async (span) => {
|
||||
span.setStatus("error", "validation failed");
|
||||
return undefined;
|
||||
});
|
||||
expect(tracer.spans[0]!.status).toBe("error");
|
||||
expect(tracer.spans[0]!.statusMessage).toBe("validation failed");
|
||||
});
|
||||
|
||||
it("reset() clears recorded spans", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
await tracer.startSpan({ name: "x" }, async () => undefined);
|
||||
expect(tracer.spans).toHaveLength(1);
|
||||
tracer.reset();
|
||||
expect(tracer.spans).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("findSpan returns first matching span by name", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
await tracer.startSpan({ name: "a" }, async () => undefined);
|
||||
await tracer.startSpan({ name: "b" }, async () => undefined);
|
||||
expect(tracer.findSpan("b")).toBeDefined();
|
||||
expect(tracer.findSpan("missing")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("nested spans are recorded in order (children appear after parent end)", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
await tracer.startSpan({ name: "parent" }, async () => {
|
||||
await tracer.startSpan({ name: "child" }, async () => undefined);
|
||||
});
|
||||
expect(tracer.spans.map((s) => s.name)).toEqual(["child", "parent"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
ITracer,
|
||||
ISpan,
|
||||
SpanOpts,
|
||||
AttributeValue,
|
||||
} from "@repo/core-shared/instrumentation";
|
||||
|
||||
export type RecordedSpan = {
|
||||
name: string;
|
||||
op?: string;
|
||||
attributes: Record<string, AttributeValue>;
|
||||
status: "ok" | "error";
|
||||
statusMessage?: string;
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
export class RecordingTracer implements ITracer {
|
||||
spans: RecordedSpan[] = [];
|
||||
|
||||
async startSpan<T>(opts: SpanOpts, fn: (span: ISpan) => Promise<T>): Promise<T> {
|
||||
const start = performance.now();
|
||||
const recorded: RecordedSpan = {
|
||||
name: opts.name,
|
||||
op: opts.op,
|
||||
attributes: { ...(opts.attributes ?? {}) },
|
||||
status: "ok",
|
||||
durationMs: 0,
|
||||
};
|
||||
const span: ISpan = {
|
||||
setAttribute(key, value) {
|
||||
recorded.attributes[key] = value;
|
||||
},
|
||||
setStatus(status, message) {
|
||||
recorded.status = status;
|
||||
recorded.statusMessage = message;
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await fn(span);
|
||||
recorded.durationMs = performance.now() - start;
|
||||
this.spans.push(recorded);
|
||||
return result;
|
||||
} catch (err) {
|
||||
recorded.status = "error";
|
||||
recorded.statusMessage = err instanceof Error ? err.message : String(err);
|
||||
recorded.durationMs = performance.now() - start;
|
||||
this.spans.push(recorded);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.spans = [];
|
||||
}
|
||||
|
||||
findSpan(name: string): RecordedSpan | undefined {
|
||||
return this.spans.find((s) => s.name === name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user