From 05e7bf6c571e62b81d25924c284d56d845ff398a Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 7 May 2026 00:08:23 +0200 Subject: [PATCH] feat(core-testing): RecordingLogger + ./instrumentation subpath --- packages/core-testing/package.json | 2 + packages/core-testing/src/index.ts | 1 + .../core-testing/src/instrumentation/index.ts | 2 + .../instrumentation/recording-logger.test.ts | 61 +++++++++++++++++++ .../src/instrumentation/recording-logger.ts | 47 ++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 packages/core-testing/src/instrumentation/index.ts create mode 100644 packages/core-testing/src/instrumentation/recording-logger.test.ts create mode 100644 packages/core-testing/src/instrumentation/recording-logger.ts diff --git a/packages/core-testing/package.json b/packages/core-testing/package.json index 01fddaf..2fdcc8f 100644 --- a/packages/core-testing/package.json +++ b/packages/core-testing/package.json @@ -7,6 +7,7 @@ ".": "./src/index.ts", "./factory": "./src/factory/index.ts", "./contract": "./src/contract/index.ts", + "./instrumentation": "./src/instrumentation/index.ts", "./react": "./src/react/index.ts", "./payload": "./src/payload/index.ts", "./payload/stub-config": "./src/payload/stub-config.ts", @@ -20,6 +21,7 @@ "test": "vitest run" }, "dependencies": { + "@repo/core-shared": "workspace:*", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.0", diff --git a/packages/core-testing/src/index.ts b/packages/core-testing/src/index.ts index 86bda6b..a1a03ac 100644 --- a/packages/core-testing/src/index.ts +++ b/packages/core-testing/src/index.ts @@ -1,2 +1,3 @@ export * from "./factory/index.js"; export * from "./contract/index.js"; +export * from "./instrumentation/index.js"; diff --git a/packages/core-testing/src/instrumentation/index.ts b/packages/core-testing/src/instrumentation/index.ts new file mode 100644 index 0000000..6d50b93 --- /dev/null +++ b/packages/core-testing/src/instrumentation/index.ts @@ -0,0 +1,2 @@ +export { RecordingTracer, type RecordedSpan } from "./recording-tracer"; +export { RecordingLogger, type RecordedCapture } from "./recording-logger"; diff --git a/packages/core-testing/src/instrumentation/recording-logger.test.ts b/packages/core-testing/src/instrumentation/recording-logger.test.ts new file mode 100644 index 0000000..4c95d44 --- /dev/null +++ b/packages/core-testing/src/instrumentation/recording-logger.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { RecordingLogger } from "@/instrumentation/recording-logger"; + +describe("RecordingLogger", () => { + it("records captureException calls (err + ctx)", () => { + const logger = new RecordingLogger(); + const err = new Error("x"); + logger.captureException(err, { tags: { feature: "blog" } }); + expect(logger.captures).toHaveLength(1); + expect(logger.captures[0]).toMatchObject({ + kind: "exception", + err, + ctx: { tags: { feature: "blog" } }, + }); + }); + + it("records captureMessage calls", () => { + const logger = new RecordingLogger(); + logger.captureMessage("hello", "warning", { extras: { foo: 1 } }); + expect(logger.captures[0]).toMatchObject({ + kind: "message", + message: "hello", + level: "warning", + }); + }); + + it("records breadcrumbs", () => { + const logger = new RecordingLogger(); + logger.addBreadcrumb({ category: "test", message: "x", level: "info" }); + expect(logger.breadcrumbs).toHaveLength(1); + expect(logger.breadcrumbs[0]!.category).toBe("test"); + }); + + it("records setUser calls", () => { + const logger = new RecordingLogger(); + logger.setUser({ id: "u1" }); + logger.setUser(null); + expect(logger.users).toEqual([{ id: "u1" }, null]); + }); + + it("reset() clears all recordings", () => { + const logger = new RecordingLogger(); + logger.captureException(new Error("x")); + logger.addBreadcrumb({ category: "c", message: "m" }); + logger.setUser({ id: "u" }); + logger.reset(); + expect(logger.captures).toHaveLength(0); + expect(logger.breadcrumbs).toHaveLength(0); + expect(logger.users).toHaveLength(0); + }); + + it("findCapture returns first capture matching predicate", () => { + const logger = new RecordingLogger(); + logger.captureException(new Error("first")); + logger.captureException(new Error("second")); + const found = logger.findCapture( + (c) => c.kind === "exception" && (c.err as Error).message === "second", + ); + expect(found).toBeDefined(); + }); +}); diff --git a/packages/core-testing/src/instrumentation/recording-logger.ts b/packages/core-testing/src/instrumentation/recording-logger.ts new file mode 100644 index 0000000..c4e965e --- /dev/null +++ b/packages/core-testing/src/instrumentation/recording-logger.ts @@ -0,0 +1,47 @@ +import type { + ILogger, + Breadcrumb, + CaptureContext, +} from "@repo/core-shared/instrumentation"; + +export type RecordedCapture = + | { kind: "exception"; err: unknown; ctx?: CaptureContext } + | { kind: "message"; message: string; level?: "info" | "warning" | "error"; ctx?: CaptureContext }; + +export class RecordingLogger implements ILogger { + captures: RecordedCapture[] = []; + breadcrumbs: Breadcrumb[] = []; + users: Array<{ id: string } | null> = []; + + captureException(err: unknown, ctx?: CaptureContext): void { + this.captures.push({ kind: "exception", err, ctx }); + } + + captureMessage( + message: string, + level?: "info" | "warning" | "error", + ctx?: CaptureContext, + ): void { + this.captures.push({ kind: "message", message, level, ctx }); + } + + addBreadcrumb(b: Breadcrumb): void { + this.breadcrumbs.push(b); + } + + setUser(user: { id: string } | null): void { + this.users.push(user); + } + + reset(): void { + this.captures = []; + this.breadcrumbs = []; + this.users = []; + } + + findCapture( + predicate: (c: RecordedCapture) => boolean, + ): RecordedCapture | undefined { + return this.captures.find(predicate); + } +}