import type { ILogger } from "./logger.interface"; import type { Captured } from "../conformance/brands"; import { attachBrand } from "../conformance/brand-runtime"; import { isReported, markReported } from "./reported-flag"; /** * Higher-order wrapper applied at DI bind time. Mirrors `withSpan`: takes a * factory result `(args) => Promise` and returns the same shape, but any * thrown error is captured via `logger.captureException(err, { tags })` before * being re-thrown. * * Skips capture if the error already carries the `__sentryReported` flag — * this is what prevents double-capture when the same error bubbles through * a wrapped repo → use case → controller chain (the repo's catch site * captures first; outer wrappers see the flag and bail). * * Usage at bind time: * * const captured = withCapture(logger, { feature: "blog", layer: "use-case", name: "blog.getArticles" }, factory(deps)); * const wrapped = withSpan(tracer, opts, captured); * * Span wraps capture: the span timing reflects the captured-and-rethrown * failure (errored span gets a duration), and the capture has accurate * tags by the time it fires. */ export function withCapture( logger: ILogger, tags: Record, fn: (...args: Args) => Promise, ): Captured<(...args: Args) => Promise> { const PROPAGATED_BRANDS = ["__instrumented", "__audited"] as const; const wrapped: (...args: Args) => Promise = async (...args) => { try { return await fn(...args); } catch (err) { if (!isReported(err)) { logger.captureException(err, { tags }); markReported(err); } throw err; } }; // Propagate brands from the inner function (e.g. __audited from withAudit, // __instrumented if already spanned) so the outermost binding carries all brands. // __captured is omitted here because it is attached explicitly below. for (const brand of PROPAGATED_BRANDS) { if ((fn as unknown as Record)[brand] === true) { attachBrand(wrapped, brand); } } attachBrand(wrapped, "__captured"); return wrapped as Captured<(...args: Args) => Promise>; }