docs: instrumentation conventions in CLAUDE.md / AGENTS.md / vertical-feature-spec.md

Adds the seven Plan 10 conventions to CLAUDE.md (interfaces in core-shared,
spans at bind time, throw-site capture, PII rules, three Sentry projects,
orthogonal binding). Adds an "Instrumentation conventions" section to
AGENTS.md with repo constructor/method patterns, capture-rules table,
boundary allowlist, and test rules. Appends §16 "Instrumentation & error
capture" to vertical-feature-spec.md (the spec already has 15 sections,
so appending rather than slotting in as §10 to avoid renumbering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:33:24 +02:00
parent d4b23cf35d
commit c640cdf6c8
3 changed files with 107 additions and 0 deletions

View File

@@ -348,6 +348,83 @@ Actual function names: `bindProductionAuth`, `bindProductionBlog`, `bindProducti
---
## Instrumentation conventions
**Symbols (in `core-shared/instrumentation/symbols.ts`):**
- `INSTRUMENTATION_SYMBOLS.TRACER` — bound to `ITracer` (`NoopTracer` / `SentryTracer`)
- `INSTRUMENTATION_SYMBOLS.LOGGER` — bound to `ILogger` (`NoopLogger` / `SentryLogger`)
**Repository constructor signature (every feature):**
```ts
constructor(
config: SanitizedConfig,
tracer: ITracer = new NoopTracer(),
logger: ILogger = new NoopLogger(),
)
```
**Repository method body (every public async method):**
```ts
return this.tracer.startSpan(
{ name: "<entity>.<method>", op: "repository", attributes: { /* ... */ } },
async (span) => {
try {
const result = await /* payload op */;
span.setAttribute("count", /* ... */);
return result;
} catch (err) {
this.logger.captureException(err, {
tags: { feature: "<feature>", repo: "<entity>", method: "<method>" },
});
span.setStatus("error", err instanceof Error ? err.message : String(err));
throw err;
}
},
);
```
**Use case + controller spans (applied at DI bind time):**
```ts
const wrappedUC = withSpan(
tracer,
{ name: "blog.getArticles", op: "use-case" },
getArticlesUseCase(repo),
);
const wrappedCtrl = withSpan(
tracer,
{ name: "blog.getArticles", op: "controller" },
getArticlesController(wrappedUC),
);
```
**Capture rules:**
| Layer | Captures | Doesn't capture |
|---|---|---|
| Repository | Infra/Payload errors that originate here | Bubbled errors |
| Use case | Business-rule violations originated in this body | Errors from repos (already captured) |
| Controller | `InputParseError` from safeParse failure | Anything else |
| `defineErrorMiddleware` | Nothing — maps domain → TRPCError only | — |
**Boundary rule (eslint-enforced, R40):**
Feature packages MUST NOT `import "@sentry/*"`. Allowlist:
- `**/instrumentation/sentry/**` (core-shared)
- `**/instrumentation/di/bind-sentry-instrumentation.{ts,test.ts}`
- `**/setup/no-sentry.{ts,js}` + the test guard
- `apps/*/instrumentation*.{ts,mjs,js}`
- `apps/*/next.config.{mjs,ts,js}`
- `apps/*/vite.config.{ts,mjs,js}`
**Test rules:**
- Default to `NoopTracer` / `NoopLogger` (constructor defaults)
- Assert spans/captures by injecting `RecordingTracer` / `RecordingLogger` from `@repo/core-testing/instrumentation`
- Real `@sentry/*` SDK MUST NOT initialize during tests (guarded by `core-testing/setup/no-sentry.ts`)
---
## Specification & Guides
- **Vertical Feature Spec** — `docs/architecture/vertical-feature-spec.md` — full design, rationale, decision log