docs: refresh architecture references for OTel migration
This commit is contained in:
@@ -2378,12 +2378,13 @@ footer .colophon {
|
||||
</div>
|
||||
|
||||
<h3 class="trace-h3">The trace tree (one tRPC request)</h3>
|
||||
<pre class="trace-tree"><code>HTTP transaction (auto, @sentry/nextjs)
|
||||
└── tRPC procedure span (auto, sentry trpc integration)
|
||||
└── controller span (op="controller", composed at DI bind time)
|
||||
└── use-case span (op="use-case", composed at DI bind time)
|
||||
└── repository span (op="repository", inline per method)
|
||||
└── Payload Local API call (auto, @sentry/node http)</code></pre>
|
||||
<p class="trace-p"><strong>Substrate: OpenTelemetry SDK</strong> (ADR-017). Sentry is the exporter via <code>@sentry/opentelemetry</code>. Auto-instrumentations cover HTTP, undici, and pg; feature code emits via <code>ITracer</code> / <code>ILogger</code> interfaces only.</p>
|
||||
<pre class="trace-tree"><code>HTTP transaction (auto, OTel HttpInstrumentation)
|
||||
└── tRPC procedure span (auto, OTel + Sentry tRPC integration)
|
||||
└── controller span (op="controller", composed at DI bind time via OtelTracer)
|
||||
└── use-case span (op="use-case", composed at DI bind time via OtelTracer)
|
||||
└── repository span (op="repository", inline per method via ITracer)
|
||||
└── Payload Local API call (auto, OTel PgInstrumentation / UndiciInstrumentation)</code></pre>
|
||||
|
||||
<h3 class="trace-h3">Where instrumentation actually lives</h3>
|
||||
<p class="trace-p">Two ways spans + captures get attached. <strong>Inline</strong> means the call appears in the layer's own body. <strong>Composed-in</strong> means a higher-order wrapper applied at DI bind time — the body stays vendor-clean.</p>
|
||||
@@ -2502,16 +2503,16 @@ const wrappedCtrl = withSpan(
|
||||
</table>
|
||||
|
||||
<h3 class="trace-h3">Double-report guard</h3>
|
||||
<p class="trace-p">Each error gets a non-enumerable <code>__sentryReported</code> flag the first time it's captured. <code>withCapture</code>, <code>SentryLogger</code>, and <code>RecordingLogger</code> all check the flag and bail if it's set. So an error bubbling repo → use-case → controller surfaces in the logger <strong>exactly once</strong>, with the inner-most layer's tags. Helper lives in <code>core-shared/instrumentation/reported-flag.ts</code>.</p>
|
||||
<p class="trace-p">Each error gets a non-enumerable <code>__sentryReported</code> flag the first time it's captured. <code>withCapture</code>, <code>OtelLogger</code>, and <code>RecordingLogger</code> all check the flag and bail if it's set. So an error bubbling repo → use-case → controller surfaces in the logger <strong>exactly once</strong>, with the inner-most layer's tags. Helper lives in <code>core-shared/instrumentation/reported-flag.ts</code>.</p>
|
||||
|
||||
<h3 class="trace-h3">PII rules (R31–R38, non-negotiable)</h3>
|
||||
<ul class="pii-rules">
|
||||
<li><code>sendDefaultPii: false</code> — every <code>Sentry.init()</code>. CI grep gate.</li>
|
||||
<li>Replay <strong>default-masks all text + inputs + media</strong>. Allowlist starts empty.</li>
|
||||
<li><code>beforeSend</code> scrubber strips email / password / token / cookie / authorization / ipaddress keys (substring match).</li>
|
||||
<li><code>beforeSendTransaction</code> scrubber strips PII query params from URLs.</li>
|
||||
<li><code>setUser</code> accepts only <code>{ id }</code>. Stripping wrapper warns in dev when other keys passed.</li>
|
||||
<li>IPv4/IPv6 in event payload string values redacted to <code>[redacted-ip]</code>.</li>
|
||||
<li><strong>Server-side:</strong> <code>PiiScrubSpanProcessor</code> + <code>PiiScrubLogRecordProcessor</code> run FIRST in the OTel processor chain — attribute-key substring match strips email / password / token / cookie / authorization / ipaddress keys before the Sentry exporter sees the data (R32, R33, ADR-017 §7).</li>
|
||||
<li><strong>Browser-side:</strong> <code>beforeSend</code> / <code>beforeSendTransaction</code> hooks in <code>init-client*.ts</code> strip the same PII keys (browser does not use the OTel pipeline).</li>
|
||||
<li><code>setUser</code> accepts only <code>{ id }</code>. No email/username (R36).</li>
|
||||
<li>IPv4/IPv6 redacted to <code>[redacted-ip]</code> in browser-side scrubbers.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -94,18 +94,21 @@ Three layers work in tandem:
|
||||
|
||||
The two enforcement layers are independent but complementary. ESLint is stricter on per-import context (e.g., file-specific exemptions via `// @boundaries-ignore`), while Turborepo catches transitive issues that lint-time checking misses. Run `pnpm lint` and `pnpm turbo boundaries` in CI to catch all violations.
|
||||
|
||||
## TRACER / LOGGER (Plan 10)
|
||||
## TRACER / LOGGER / METRICS (ADR-014, ADR-017)
|
||||
|
||||
The instrumentation layer is **per-feature container** but **app-wide instance**: each feature container binds `INSTRUMENTATION_SYMBOLS.TRACER` and `INSTRUMENTATION_SYMBOLS.LOGGER` to the SAME instance, constructed once by the app's `bindAll()` dispatcher (Rule 0).
|
||||
The instrumentation layer is **per-feature container** but **app-wide instance**: each feature container binds `INSTRUMENTATION_SYMBOLS.ITracer`, `INSTRUMENTATION_SYMBOLS.ILogger`, and `INSTRUMENTATION_SYMBOLS.IMetrics` to the SAME instances, constructed once by the app's `bindAll()` dispatcher (Rule 0).
|
||||
|
||||
**Substrate:** OpenTelemetry SDK. Sentry is the exporter via `@sentry/opentelemetry`. PII scrubbing runs at the OTel processor layer (`PiiScrubSpanProcessor` + `PiiScrubLogRecordProcessor`) before the Sentry exporter sees the data. Feature code is never coupled to the OTel SDK or Sentry SDK directly (ADR-017).
|
||||
|
||||
```
|
||||
apps/web-next/src/server/bind-production.ts (bindAll)
|
||||
│
|
||||
├─ Rule 0: WEB_NEXT_SENTRY_DSN set?
|
||||
│ yes → bindSentryInstrumentation(sharedContainer, { dsn, app: "web-next" })
|
||||
│ yes → bindOtelInstrumentation(sharedContainer, { dsn, app: "web-next" })
|
||||
│ → initOtelServerNode(dsn, ...) → OTel SDK + Sentry exporter + PII scrub processors
|
||||
│ no → bindNoopInstrumentation(sharedContainer)
|
||||
│ ↓
|
||||
│ tracer + logger instances
|
||||
│ tracer (OtelTracer) + logger (OtelLogger) + metrics (OtelMetrics) instances
|
||||
│ ↓
|
||||
├─ resolveEventsAndJobs* → IEventBus + IJobQueue (ADR-015)
|
||||
│ production → PayloadJobsEventBus + PayloadJobQueue
|
||||
@@ -115,9 +118,9 @@ apps/web-next/src/server/bind-production.ts (bindAll)
|
||||
│ server.ts → SocketIORealtimeBroadcaster + RealtimeHandlerRegistry (passed in from server.ts)
|
||||
│ page/test → InMemoryRealtimeBroadcaster + RealtimeHandlerRegistry (defaults)
|
||||
│ ↓
|
||||
├─ build ctx: BindProductionContext = { config, tracer, logger, bus, queue, realtime, realtimeRegistry }
|
||||
├─ build ctx: BindProductionContext = { config, tracer, logger, metrics?, bus, queue, realtime, realtimeRegistry }
|
||||
│ Required: tracer, logger, config (production only)
|
||||
│ Optional: bus, queue, realtime, realtimeRegistry (guard with ?. when used)
|
||||
│ Optional: metrics, bus, queue, realtime, realtimeRegistry (guard with ?. when used)
|
||||
│ ↓
|
||||
├─ bindProductionBlog(ctx: BindProductionContext)
|
||||
│ │
|
||||
@@ -138,8 +141,9 @@ apps/web-next/src/server/bind-production.ts (bindAll)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `tracer` | `ITracer` | always | Resolved by Rule 0 (Sentry vs Noop) |
|
||||
| `tracer` | `ITracer` | always | Resolved by Rule 0 (OTel+Sentry vs Noop) |
|
||||
| `logger` | `ILogger` | always | Resolved by Rule 0 |
|
||||
| `metrics` | `MetricsProtocol?` | optional | Resolved by Rule 0; per-feature adoption is opportunistic |
|
||||
| `config` | `SanitizedConfig` | production only | Present in `BindProductionContext`, absent in `BindContext` |
|
||||
| `bus` | `EventBusProtocol?` | optional | `IEventBus` at the aggregator; protocol surface at binders |
|
||||
| `queue` | `IJobQueue?` | optional | Present when `core-shared/jobs` is wired |
|
||||
@@ -150,6 +154,6 @@ Feature binders destructure `ctx` and use optional fields with `?.` or cast to t
|
||||
|
||||
**Why per-feature containers also get the binding:** lets internal DI-resolved code in a feature pull TRACER/LOGGER without going through the app dispatcher. In practice, only repository classes and feature-internal services would use this — controllers and use cases receive instrumentation via the bind-time wrapper.
|
||||
|
||||
**Why the shared container exists at all:** isolates Rule 0 resolution from feature containers. Feature containers don't need to know if Sentry is on or off — they just receive an `ITracer` instance.
|
||||
**Why the shared container exists at all:** isolates Rule 0 resolution from feature containers. Feature containers don't need to know if Sentry is the exporter or not — they just receive an `ITracer` instance.
|
||||
|
||||
**Boundary rule:** feature packages MUST NOT import `@sentry/*` directly (R40, ESLint-enforced). The only paths that may import the SDK are `core-shared/instrumentation/sentry/**`, the `bind-sentry-instrumentation` files, the `no-sentry` test guards, and per-app `instrumentation*.{ts,mjs}` / `next.config.{mjs}` / `vite.config.{ts}` entries.
|
||||
**Boundary rule:** feature packages MUST NOT import `@sentry/*` or `@opentelemetry/sdk-*` directly (R40 + R52, ESLint-enforced). The OTel bridge (`otel/sentry-bridge.ts`), browser init files (`sentry/init-client*.ts`), and app-level `instrumentation*.{ts,mjs}` / `next.config.{mjs}` / `vite.config.{ts}` entries are the only allowlisted paths.
|
||||
|
||||
@@ -1170,28 +1170,32 @@ footer .colophon {
|
||||
<div class="section-num">§ 08</div>
|
||||
<div>
|
||||
<h2 class="section-title">Instrumentation <em>symbols</em>.</h2>
|
||||
<p class="section-blurb">Plan 10 added two new symbols to the per-feature container — <code>TRACER</code> and <code>LOGGER</code> — bound by a separate Rule 0 in <code>bindAll()</code> that's <strong>orthogonal</strong> to the repo binding mode. The DSN env var decides Sentry vs Noop; <code>USE_DEV_SEED</code> / <code>NODE_ENV</code> decide real vs mock repos.</p>
|
||||
<p class="section-blurb">ADR-014 + ADR-017 added instrumentation symbols to the per-feature container — <code>ITracer</code>, <code>ILogger</code>, and <code>IMetrics</code> — bound by a separate Rule 0 in <code>bindAll()</code> that's <strong>orthogonal</strong> to the repo binding mode. The DSN env var decides OTel+Sentry vs Noop; <code>USE_DEV_SEED</code> / <code>NODE_ENV</code> decide real vs mock repos. <strong>Substrate: OpenTelemetry SDK.</strong> Sentry is the exporter via <code>@sentry/opentelemetry</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instrumentation-grid">
|
||||
<div>
|
||||
<h3 class="trace-h3"><code>INSTRUMENTATION_SYMBOLS.TRACER</code></h3>
|
||||
<p class="trace-p">Bound by either <code>bindNoopInstrumentation</code> or <code>bindSentryInstrumentation</code> to <code>NoopTracer</code> or <code>SentryTracer</code>. Decided by Rule 0: DSN env present → Sentry; otherwise Noop.</p>
|
||||
<h3 class="trace-h3"><code>INSTRUMENTATION_SYMBOLS.ITracer</code></h3>
|
||||
<p class="trace-p">Bound by either <code>bindNoopInstrumentation</code> or <code>bindOtelInstrumentation</code> to <code>NoopTracer</code> or <code>OtelTracer</code>. Decided by Rule 0: DSN env present → OTel SDK + Sentry exporter; otherwise Noop. <code>OtelTracer</code> emits via <code>@opentelemetry/api</code>; spans flow to Sentry via <code>SentrySpanProcessor</code>.</p>
|
||||
|
||||
<h3 class="trace-h3"><code>INSTRUMENTATION_SYMBOLS.LOGGER</code></h3>
|
||||
<p class="trace-p">Same rule, same lifecycle. <code>NoopLogger</code> in the absence of a DSN; <code>SentryLogger</code> when DSN is set. The Sentry adapter applies the <code>__sentryReported</code> double-report guard internally — call sites don't manage the flag.</p>
|
||||
<h3 class="trace-h3"><code>INSTRUMENTATION_SYMBOLS.ILogger</code></h3>
|
||||
<p class="trace-p">Same rule, same lifecycle. <code>NoopLogger</code> in the absence of a DSN; <code>OtelLogger</code> when DSN is set. <code>OtelLogger</code> emits log records via <code>@opentelemetry/api-logs</code>; errors flow to Sentry via <code>SentryLogRecordProcessor</code>. The <code>__sentryReported</code> double-report guard is applied before emitting.</p>
|
||||
|
||||
<h3 class="trace-h3"><code>INSTRUMENTATION_SYMBOLS.IMetrics</code></h3>
|
||||
<p class="trace-p">Bound to <code>NoopMetrics</code> or <code>OtelMetrics</code>. <code>OtelMetrics</code> uses the OTel metrics API (<code>counter</code>, <code>histogram</code>, <code>gauge</code>). Per-feature adoption is opportunistic — no feature call sites required.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="trace-h3">Wiring path</h3>
|
||||
<pre class="trace-tree"><code>bindAll()
|
||||
└─ resolveInstrumentation() ← Rule 0 (DSN check)
|
||||
└─ Noop or Sentry binders ← bind to sharedContainer
|
||||
└─ resolveInstrumentation() ← Rule 0 (DSN check)
|
||||
└─ bindOtelInstrumentation() ← OTel SDK init + Sentry exporter + PII scrub processors
|
||||
OR bindNoopInstrumentation() ← all Noop
|
||||
└─ resolveEventsAndJobs*() ← ADR-015 (env-driven bus + queue)
|
||||
└─ Payload-backed in prod, in-memory in dev-seed
|
||||
└─ build ctx: BindProductionContext = { config, tracer, logger, bus, queue, realtime, realtimeRegistry }
|
||||
└─ required: tracer, logger, config | optional: bus, queue, realtime, realtimeRegistry
|
||||
└─ build ctx: BindProductionContext = { config, tracer, logger, metrics?, bus, queue, realtime, realtimeRegistry }
|
||||
└─ required: tracer, logger, config | optional: metrics, bus, queue, realtime, realtimeRegistry
|
||||
└─ bindProductionX(ctx) ← single ctx object passed to each feature binder
|
||||
└─ feature container also binds TRACER + LOGGER
|
||||
└─ withSpan(withCapture(...)) at every use case + controller
|
||||
|
||||
@@ -685,15 +685,17 @@ Invoke the `superpowers:writing-plans` skill to produce a detailed, executable i
|
||||
|
||||
---
|
||||
|
||||
## 16. Instrumentation & error capture (Plan 10)
|
||||
## 16. Instrumentation & error capture (ADR-014, ADR-017)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-06-instrumentation-sentry-design.md` (R31–R55).
|
||||
**Spec:** `docs/superpowers/specs/2026-05-06-instrumentation-sentry-design.md` (R31–R55 interfaces); `docs/decisions/adr-017-opentelemetry-migration.md` (OTel substrate, supersedes ADR-014 impl section).
|
||||
|
||||
**Substrate:** OpenTelemetry SDK. Sentry is the exporter via `@sentry/opentelemetry`. PII scrubbing happens at the OTel processor layer before the Sentry exporter. Feature code depends only on `ITracer`, `ILogger`, `IMetrics` interfaces — no Sentry or OTel SDK imports.
|
||||
|
||||
**File additions per feature:**
|
||||
|
||||
- `infrastructure/repositories/<entity>.repository.ts` — constructor takes `(config, tracer, logger)` with Noop defaults; every public method's body is wrapped in `tracer.startSpan(...)` and any `catch` block calls `logger.captureException(err, { tags: { feature, repo, method } })` before re-throwing.
|
||||
- `infrastructure/repositories/<entity>.repository.mock.ts` — same constructor/wrapping shape (no catch — mocks don't originate infra errors).
|
||||
- `di/bind-production.ts` — signature `(ctx: BindProductionContext)` (from `@repo/core-shared/di`). Destructures `{ config, tracer, logger, bus, queue, realtime, realtimeRegistry }` from `ctx`. Binds TRACER + LOGGER to the feature container; constructs the real repo with tracer/logger; wraps every use case + controller via `withSpan(withCapture(factory(deps)))` at bind time. `withSpan` is outermost so an errored span's timing reflects the capture-and-rethrow; `withCapture` honours the `__sentryReported` flag so a bubbled error from the repo isn't re-captured. Optional fields (`bus`, `queue`, `realtime`, `realtimeRegistry`) are guarded with `?.` or cast to the full interface when the feature unconditionally requires them.
|
||||
- `di/bind-production.ts` — signature `(ctx: BindProductionContext)` (from `@repo/core-shared/di`). Destructures `{ config, tracer, logger, metrics?, bus, queue, realtime, realtimeRegistry }` from `ctx`. Binds TRACER + LOGGER to the feature container; constructs the real repo with tracer/logger; wraps every use case + controller via `withSpan(withCapture(factory(deps)))` at bind time. `withSpan` is outermost so an errored span's timing reflects the capture-and-rethrow; `withCapture` honours the `__sentryReported` flag so a bubbled error from the repo isn't re-captured. Optional fields (`metrics`, `bus`, `queue`, `realtime`, `realtimeRegistry`) are guarded with `?.` or cast to the full interface when the feature unconditionally requires them.
|
||||
- `di/bind-dev-seed.ts` — signature `(ctx: BindContext)` (no `config`). Same wrapping as bind-production but with the populated mock.
|
||||
|
||||
**Required exports (per feature root):** unchanged.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# ADR-014 — Instrumentation & Sentry Logging
|
||||
|
||||
**Status:** Accepted
|
||||
**Status (revised):** Superseded by ADR-017 for the implementation layer. The interface decisions (R31–R51) remain authoritative.
|
||||
**Date:** 2026-05-06
|
||||
**Spec:** docs/superpowers/specs/2026-05-06-instrumentation-sentry-design.md
|
||||
**Plan:** docs/superpowers/plans/2026-05-06-plan-10-instrumentation-sentry.md
|
||||
|
||||
Reference in New Issue
Block a user