Files
agentic-dev/packages/media/src/di/bind-production.ts
Danijel Martinek f0775d6ecc feat(instrumentation): close R44 gap — throw-site capture for use cases + controllers
Plan 10 documented R44 (capture at originating-throw layer) but only the
R43 repo leg was wired. captureException had zero call sites in any
controller or use-case body. This commit closes the gap.

Mechanism:
- Extract __sentryReported flag helpers into core-shared/instrumentation/
  reported-flag.ts. SentryLogger switches to importing them; RecordingLogger
  carries an inlined copy (tooling → core boundary disallows the import).
- Add withCapture(logger, tags, fn) higher-order wrapper paralleling
  withSpan. On throw: capture-with-tags, mark, re-throw. Bail if the flag
  was already set — covers the bubbled-from-repo case so each error
  surfaces in the logger exactly once with the inner-most layer's tags.
- Apply withSpan(withCapture(factory)) in every feature's bind-production
  and bind-dev-seed: auth (3 use cases × 3 controllers), blog (3×3),
  marketing-pages (2×2), navigation (1×1), media (3×3). Span is outermost
  so the errored span timing reflects the capture-and-rethrow.
- RecordingLogger.captureException now also honours the flag — test
  capture counts stay honest when both repo and outer layer wrap.

Tests:
- packages/core-shared/src/instrumentation/with-capture.test.ts —
  4 cases covering success, capture-on-throw, mark-on-capture, no-double
  via the flag.
- packages/blog/tests/r44-no-double-capture.test.ts — 3 cases: repo throw
  → 1 capture with repo tags; controller parse fail → 1 capture with
  controller tags; success → 0 captures.

Verification: pnpm test 26/26, pnpm lint 15/15, pnpm typecheck 14/14.

Docs: ADR-014 and the refactor log gain a "Post-merge follow-up" section
recording the gap, the fix, and the underlying lesson (don't describe
intent as shipped state — grep first).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:28:22 +02:00

131 lines
4.3 KiB
TypeScript

import type { SanitizedConfig } from "payload";
import {
withSpan,
withCapture,
INSTRUMENTATION_SYMBOLS,
type ITracer,
type ILogger,
} from "@repo/core-shared/instrumentation";
import { mediaContainer } from "./container";
import { MEDIA_SYMBOLS } from "./symbols";
import { MediaRepository } from "../infrastructure/repositories/media.repository";
import { getMediaUseCase } from "../application/use-cases/get-media.use-case";
import { listMediaUseCase } from "../application/use-cases/list-media.use-case";
import { deleteMediaUseCase } from "../application/use-cases/delete-media.use-case";
import { getMediaController } from "../interface-adapters/controllers/get-media.controller";
import { listMediaController } from "../interface-adapters/controllers/list-media.controller";
import { deleteMediaController } from "../interface-adapters/controllers/delete-media.controller";
export function bindProductionMedia(
config: SanitizedConfig,
tracer: ITracer,
logger: ILogger,
): void {
// Bind shared instrumentation into feature container
if (mediaContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
mediaContainer.unbind(INSTRUMENTATION_SYMBOLS.TRACER);
}
if (mediaContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
mediaContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
}
mediaContainer.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
mediaContainer.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
// Real repository
if (mediaContainer.isBound(MEDIA_SYMBOLS.IMediaRepository)) {
mediaContainer.unbind(MEDIA_SYMBOLS.IMediaRepository);
}
const repo = new MediaRepository(config, tracer, logger);
mediaContainer
.bind(MEDIA_SYMBOLS.IMediaRepository)
.toConstantValue(repo);
// Use cases — wrapped with span + capture at bind time
const wrappedGetMedia = withSpan(
tracer,
{ name: "media.getMedia", op: "use-case" },
withCapture(
logger,
{ feature: "media", layer: "use-case", name: "media.getMedia" },
getMediaUseCase(repo),
),
);
const wrappedListMedia = withSpan(
tracer,
{ name: "media.listMedia", op: "use-case" },
withCapture(
logger,
{ feature: "media", layer: "use-case", name: "media.listMedia" },
listMediaUseCase(repo),
),
);
const wrappedDeleteMedia = withSpan(
tracer,
{ name: "media.deleteMedia", op: "use-case" },
withCapture(
logger,
{ feature: "media", layer: "use-case", name: "media.deleteMedia" },
deleteMediaUseCase(repo),
),
);
for (const sym of [
MEDIA_SYMBOLS.IGetMediaUseCase,
MEDIA_SYMBOLS.IListMediaUseCase,
MEDIA_SYMBOLS.IDeleteMediaUseCase,
]) {
if (mediaContainer.isBound(sym)) mediaContainer.unbind(sym);
}
mediaContainer.bind(MEDIA_SYMBOLS.IGetMediaUseCase).toConstantValue(wrappedGetMedia);
mediaContainer.bind(MEDIA_SYMBOLS.IListMediaUseCase).toConstantValue(wrappedListMedia);
mediaContainer.bind(MEDIA_SYMBOLS.IDeleteMediaUseCase).toConstantValue(wrappedDeleteMedia);
// Controllers — wrapped with span at bind time
for (const sym of [
MEDIA_SYMBOLS.IGetMediaController,
MEDIA_SYMBOLS.IListMediaController,
MEDIA_SYMBOLS.IDeleteMediaController,
]) {
if (mediaContainer.isBound(sym)) mediaContainer.unbind(sym);
}
mediaContainer
.bind(MEDIA_SYMBOLS.IGetMediaController)
.toConstantValue(
withSpan(
tracer,
{ name: "media.getMedia", op: "controller" },
withCapture(
logger,
{ feature: "media", layer: "controller", name: "media.getMedia" },
getMediaController(wrappedGetMedia),
),
),
);
mediaContainer
.bind(MEDIA_SYMBOLS.IListMediaController)
.toConstantValue(
withSpan(
tracer,
{ name: "media.listMedia", op: "controller" },
withCapture(
logger,
{ feature: "media", layer: "controller", name: "media.listMedia" },
listMediaController(wrappedListMedia),
),
),
);
mediaContainer
.bind(MEDIA_SYMBOLS.IDeleteMediaController)
.toConstantValue(
withSpan(
tracer,
{ name: "media.deleteMedia", op: "controller" },
withCapture(
logger,
{ feature: "media", layer: "controller", name: "media.deleteMedia" },
deleteMediaController(wrappedDeleteMedia),
),
),
);
}