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:
@@ -25,10 +25,12 @@
|
|||||||
"@repo/navigation": "workspace:*",
|
"@repo/navigation": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
|
"inversify": "^6.2.0",
|
||||||
"next": "^15.3.0",
|
"next": "^15.3.0",
|
||||||
"payload": "^3.14.0",
|
"payload": "^3.14.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"superjson": "^2.2.1"
|
"superjson": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -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.
|
// 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 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 { bindProductionBlog } from "@repo/blog/di/bind-production";
|
||||||
import { bindProductionAuth } from "@repo/auth/di/bind-production";
|
import { bindProductionAuth } from "@repo/auth/di/bind-production";
|
||||||
import { bindProductionMarketingPages } from "@repo/marketing-pages/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;
|
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
|
* 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> {
|
export async function bindAllProduction(): Promise<void> {
|
||||||
if (bound) return;
|
if (bound) return;
|
||||||
bound = true;
|
bound = true;
|
||||||
|
resolveInstrumentation(); // Rule 0
|
||||||
const resolvedConfig = await config;
|
const resolvedConfig = await config;
|
||||||
bindProductionAuth(resolvedConfig);
|
bindProductionAuth(resolvedConfig);
|
||||||
bindProductionBlog(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
|
* 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
|
* with realistic seed data so the running app shows non-empty UI without
|
||||||
* Payload booted. Useful for storybook, design review, and offline dev.
|
* Payload booted. Mutually exclusive with `bindAllProduction()`.
|
||||||
*
|
|
||||||
* Mutually exclusive with `bindAllProduction()` — both rebind the same symbols.
|
|
||||||
*/
|
*/
|
||||||
export async function bindAllDevSeed(): Promise<void> {
|
export async function bindAllDevSeed(): Promise<void> {
|
||||||
if (bound) return;
|
if (bound) return;
|
||||||
bound = true;
|
bound = true;
|
||||||
|
resolveInstrumentation(); // Rule 0
|
||||||
await bindDevSeedAuth();
|
await bindDevSeedAuth();
|
||||||
await bindDevSeedBlog();
|
await bindDevSeedBlog();
|
||||||
await bindDevSeedMarketingPages();
|
await bindDevSeedMarketingPages();
|
||||||
@@ -50,19 +84,12 @@ export async function bindAllDevSeed(): Promise<void> {
|
|||||||
*
|
*
|
||||||
* Resolution order (first match wins):
|
* Resolution order (first match wins):
|
||||||
*
|
*
|
||||||
* 1. `USE_DEV_SEED === "true"` → dev seed (explicit override; works in
|
* Rule 0 (always): instrumentation (Noop vs Sentry) from WEB_NEXT_SENTRY_DSN
|
||||||
* any NODE_ENV — useful for staging,
|
* presence — runs inside both bindAllProduction and
|
||||||
* storybook, design review).
|
* bindAllDevSeed via resolveInstrumentation().
|
||||||
* 2. `NODE_ENV === "production"` → real Payload via bindAllProduction(config).
|
* Rule 1: USE_DEV_SEED === "true" → dev seed (explicit override)
|
||||||
* 3. otherwise → dev seed (developer-friendly default;
|
* Rule 2: NODE_ENV === "production" → real Payload via bindAllProduction
|
||||||
* `pnpm dev` works without Payload booted).
|
* Rule 3: otherwise → dev seed (developer-friendly default)
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
export async function bindAll(): Promise<void> {
|
export async function bindAll(): Promise<void> {
|
||||||
if (process.env.USE_DEV_SEED === "true") {
|
if (process.env.USE_DEV_SEED === "true") {
|
||||||
@@ -73,7 +100,20 @@ export async function bindAll(): Promise<void> {
|
|||||||
await bindAllProduction();
|
await bindAllProduction();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Default for dev / test / unset NODE_ENV: dev seed so the app boots
|
|
||||||
// without Payload running.
|
|
||||||
await bindAllDevSeed();
|
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
6
pnpm-lock.yaml
generated
@@ -166,6 +166,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.16.0(typescript@5.9.3)
|
version: 11.16.0(typescript@5.9.3)
|
||||||
|
inversify:
|
||||||
|
specifier: ^6.2.0
|
||||||
|
version: 6.2.2(reflect-metadata@0.2.2)
|
||||||
next:
|
next:
|
||||||
specifier: ^15.3.0
|
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)
|
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:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.4(react@19.2.4)
|
version: 19.2.4(react@19.2.4)
|
||||||
|
reflect-metadata:
|
||||||
|
specifier: ^0.2.2
|
||||||
|
version: 0.2.2
|
||||||
superjson:
|
superjson:
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.6
|
version: 2.2.6
|
||||||
|
|||||||
Reference in New Issue
Block a user