// R44 — verify the full chain (controller → use case → repo) wraps with // withSpan + withCapture and never double-captures the same error. // // Each layer's withCapture catch checks the __sentryReported flag (set by // the inner-most layer that captured first) and skips. End result: one // error → one logger.captureException call, with the inner-most tags. import { describe, it, expect } from "vitest"; import { RecordingTracer, RecordingLogger, } from "@repo/core-testing/instrumentation"; import { withSpan, withCapture } from "@repo/core-shared/instrumentation"; import { MockArticlesRepository } from "../src/infrastructure/repositories/articles.repository.mock"; import { getArticleBySlugUseCase } from "../src/application/use-cases/get-article-by-slug.use-case"; import { getArticleBySlugController } from "../src/interface-adapters/controllers/get-article-by-slug.controller"; describe("R44 — no double-capture across span/capture-wrapped layers", () => { it("an error originated in the repo is captured exactly once with repo tags", async () => { const tracer = new RecordingTracer(); const logger = new RecordingLogger(); // Repo throws on getArticleBySlug for a special slug. We simulate the // throw via a subclass — the production repos call captureException // inside their own try/catch as part of the repo span body. class ThrowingRepo extends MockArticlesRepository { override getArticleBySlug = async (_slug: string) => { const err = new Error("boom from repo"); // Mirror what the real repo does: capture with repo tags, mark the // flag (RecordingLogger.captureException does this for us now). logger.captureException(err, { tags: { feature: "blog", repo: "articles", method: "getArticleBySlug" }, }); throw err; }; } const repo = new ThrowingRepo(tracer, logger); // Wire the use case + controller exactly the way bind-production does. const wrappedUC = withSpan( tracer, { name: "blog.getArticleBySlug", op: "use-case" }, withCapture( logger, { feature: "blog", layer: "use-case", name: "blog.getArticleBySlug" }, getArticleBySlugUseCase(repo), ), ); const wrappedCtrl = withSpan( tracer, { name: "blog.getArticleBySlug", op: "controller" }, withCapture( logger, { feature: "blog", layer: "controller", name: "blog.getArticleBySlug" }, getArticleBySlugController(wrappedUC), ), ); await expect(wrappedCtrl({ slug: "anything" })).rejects.toThrow("boom from repo"); // R44: exactly one capture. Outer wrappers saw the flag and skipped. expect(logger.captures).toHaveLength(1); const only = logger.captures[0]; expect(only?.kind).toBe("exception"); if (only?.kind !== "exception") return; expect(only.ctx?.tags).toEqual({ feature: "blog", repo: "articles", method: "getArticleBySlug", }); }); it("an error originated in the controller (parse failure) is captured once with controller tags", async () => { const tracer = new RecordingTracer(); const logger = new RecordingLogger(); const repo = new MockArticlesRepository(); const wrappedUC = withSpan( tracer, { name: "blog.getArticleBySlug", op: "use-case" }, withCapture( logger, { feature: "blog", layer: "use-case", name: "blog.getArticleBySlug" }, getArticleBySlugUseCase(repo), ), ); const wrappedCtrl = withSpan( tracer, { name: "blog.getArticleBySlug", op: "controller" }, withCapture( logger, { feature: "blog", layer: "controller", name: "blog.getArticleBySlug" }, getArticleBySlugController(wrappedUC), ), ); // safeParse fails because slug is missing. await expect(wrappedCtrl({})).rejects.toThrow(); expect(logger.captures).toHaveLength(1); const only = logger.captures[0]; expect(only?.kind).toBe("exception"); if (only?.kind !== "exception") return; expect(only.ctx?.tags).toEqual({ feature: "blog", layer: "controller", name: "blog.getArticleBySlug", }); }); it("a successful call captures nothing", async () => { const tracer = new RecordingTracer(); const logger = new RecordingLogger(); const repo = new MockArticlesRepository(); const wrappedUC = withSpan( tracer, { name: "blog.getArticles", op: "use-case" }, withCapture( logger, { feature: "blog", layer: "use-case", name: "blog.getArticles" }, async () => [], ), ); const wrappedCtrl = withSpan( tracer, { name: "blog.getArticles", op: "controller" }, withCapture( logger, { feature: "blog", layer: "controller", name: "blog.getArticles" }, async () => wrappedUC(), ), ); await expect(wrappedCtrl()).resolves.toEqual([]); expect(logger.captures).toHaveLength(0); void repo; }); });