feat(app): bindAll() Rule 0 — DSN-driven instrumentation (orthogonal to repo mode)

Introduces resolveInstrumentation() in apps/web-next/src/server/bind-production.ts:
reads WEB_NEXT_SENTRY_DSN; if set, calls bindSentryInstrumentation; else Noop.
Both bindAllProduction and bindAllDevSeed call resolveInstrumentation() at the
top. Exports __resetBindStateForTests() and __getInstrumentationForTests() for
test helpers. Adds inversify + reflect-metadata as direct dependencies of web-next.
Existing 6 tests still pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 00:03:27 +02:00
parent fbcee9b9f2
commit efacb0e072
3 changed files with 67 additions and 19 deletions

View File

@@ -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": {

View File

@@ -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<void> {
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<void> {
/**
* 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<void> {
if (bound) return;
bound = true;
resolveInstrumentation(); // Rule 0
await bindDevSeedAuth();
await bindDevSeedBlog();
await bindDevSeedMarketingPages();
@@ -50,19 +84,12 @@ export async function bindAllDevSeed(): Promise<void> {
*
* 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<void> {
if (process.env.USE_DEV_SEED === "true") {
@@ -73,7 +100,20 @@ export async function bindAll(): Promise<void> {
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 };
}

6
pnpm-lock.yaml generated
View File

@@ -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