diff --git a/apps/web-next/package.json b/apps/web-next/package.json index b05afae..14cebc0 100644 --- a/apps/web-next/package.json +++ b/apps/web-next/package.json @@ -25,10 +25,12 @@ "@repo/navigation": "workspace:*", "@tanstack/react-query": "^5.66.0", "@trpc/server": "^11.0.0", + "inversify": "^6.2.0", "next": "^15.3.0", "payload": "^3.14.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "reflect-metadata": "^0.2.2", "superjson": "^2.2.1" }, "devDependencies": { diff --git a/apps/web-next/src/server/bind-production.ts b/apps/web-next/src/server/bind-production.ts index 3d3bc11..6c65b29 100644 --- a/apps/web-next/src/server/bind-production.ts +++ b/apps/web-next/src/server/bind-production.ts @@ -1,5 +1,14 @@ +// apps/web-next/src/server/bind-production.ts // SERVER-ONLY: this module imports Payload config and must never be bundled into the browser. +import "reflect-metadata"; +import { Container } from "inversify"; import config from "@repo/core-cms"; +import { + bindNoopInstrumentation, + bindSentryInstrumentation, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import { bindProductionBlog } from "@repo/blog/di/bind-production"; import { bindProductionAuth } from "@repo/auth/di/bind-production"; import { bindProductionMarketingPages } from "@repo/marketing-pages/di/bind-production"; @@ -13,13 +22,39 @@ import { bindDevSeedMedia } from "@repo/media/di/bind-dev-seed"; let bound = false; +// Shared container holds TRACER + LOGGER bindings; per-feature containers +// receive references via parameter passing. This separates the instrumentation +// container (one) from feature containers (per-feature, ADR-008). +const sharedContainer = new Container(); + +let resolvedTracer: ITracer | null = null; +let resolvedLogger: ILogger | null = null; + +/** Rule 0: pick instrumentation backend from DSN env (orthogonal to repo mode). */ +function resolveInstrumentation(): { tracer: ITracer; logger: ILogger } { + if (resolvedTracer && resolvedLogger) { + return { tracer: resolvedTracer, logger: resolvedLogger }; + } + const dsn = process.env.WEB_NEXT_SENTRY_DSN; + const result = dsn + ? bindSentryInstrumentation(sharedContainer, { dsn, app: "web-next" }) + : bindNoopInstrumentation(sharedContainer); + resolvedTracer = result.tracer; + resolvedLogger = result.logger; + return result; +} + /** * Production path: swap each feature's mock repository binding for the real - * Payload-backed one. Constructs `new XRepository(config)` per feature. + * Payload-backed one. Constructs `new XRepository(config, tracer, logger)` per + * feature once Phase E feature wiring lands. Until Phase E, this still calls + * the existing `bindProductionX(config)` signature; the unused `tracer`/`logger` + * are forwarded by Phase E commits as those binders are updated. */ export async function bindAllProduction(): Promise { if (bound) return; bound = true; + resolveInstrumentation(); // Rule 0 const resolvedConfig = await config; bindProductionAuth(resolvedConfig); bindProductionBlog(resolvedConfig); @@ -31,13 +66,12 @@ export async function bindAllProduction(): Promise { /** * Dev-seed path: keep each feature's MockXRepository in place but populate it * with realistic seed data so the running app shows non-empty UI without - * Payload booted. Useful for storybook, design review, and offline dev. - * - * Mutually exclusive with `bindAllProduction()` — both rebind the same symbols. + * Payload booted. Mutually exclusive with `bindAllProduction()`. */ export async function bindAllDevSeed(): Promise { if (bound) return; bound = true; + resolveInstrumentation(); // Rule 0 await bindDevSeedAuth(); await bindDevSeedBlog(); await bindDevSeedMarketingPages(); @@ -50,19 +84,12 @@ export async function bindAllDevSeed(): Promise { * * Resolution order (first match wins): * - * 1. `USE_DEV_SEED === "true"` → dev seed (explicit override; works in - * any NODE_ENV — useful for staging, - * storybook, design review). - * 2. `NODE_ENV === "production"` → real Payload via bindAllProduction(config). - * 3. otherwise → dev seed (developer-friendly default; - * `pnpm dev` works without Payload booted). - * - * To force production locally without changing NODE_ENV, set - * `USE_DEV_SEED=false` is NOT enough — set `NODE_ENV=production` instead. - * - * Idempotent: guarded by the same `bound` flag the underlying binders share. - * Call from server entry points (route handlers, page server components, - * tRPC route handlers) before resolving any feature controller. + * Rule 0 (always): instrumentation (Noop vs Sentry) from WEB_NEXT_SENTRY_DSN + * presence — runs inside both bindAllProduction and + * bindAllDevSeed via resolveInstrumentation(). + * Rule 1: USE_DEV_SEED === "true" → dev seed (explicit override) + * Rule 2: NODE_ENV === "production" → real Payload via bindAllProduction + * Rule 3: otherwise → dev seed (developer-friendly default) */ export async function bindAll(): Promise { if (process.env.USE_DEV_SEED === "true") { @@ -73,7 +100,20 @@ export async function bindAll(): Promise { await bindAllProduction(); return; } - // Default for dev / test / unset NODE_ENV: dev seed so the app boots - // without Payload running. await bindAllDevSeed(); } + +/** Test-only resets — not exported via package. Used by bind-production.test.ts. */ +export function __resetBindStateForTests(): void { + bound = false; + resolvedTracer = null; + resolvedLogger = null; +} + +/** Test-only accessor for resolved instrumentation. */ +export function __getInstrumentationForTests(): { + tracer: ITracer | null; + logger: ILogger | null; +} { + return { tracer: resolvedTracer, logger: resolvedLogger }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bb1f28..d579764 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,9 @@ importers: '@trpc/server': specifier: ^11.0.0 version: 11.16.0(typescript@5.9.3) + inversify: + specifier: ^6.2.0 + version: 6.2.2(reflect-metadata@0.2.2) next: specifier: ^15.3.0 version: 15.5.14(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0) @@ -178,6 +181,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 superjson: specifier: ^2.2.1 version: 2.2.6