From 0a248d5be0d0e8f69cc763487e0b0c5bc7bd430e Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Thu, 7 May 2026 20:37:46 +0200 Subject: [PATCH] =?UTF-8?q?docs(html):=20data-flow-explainer=20=C2=A706=20?= =?UTF-8?q?tracing=20+=20di-explainer=20=C2=A708=20instrumentation=20symbo?= =?UTF-8?q?ls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inserts a new §06 "Tracing & error capture" between the existing "Tradeoffs by part" (§05) and the verdict (which moves to §07). Includes the trace tree (HTTP → tRPC → controller → use-case → repo → Payload), capture-rules table (where captureException fires per layer + the no-capture rule for defineErrorMiddleware), the __sentryReported double-report guard explanation, and the R31–R38 PII rule list. Adds matching CSS (.trace-tree / .capture-rules / .pii-rules) tuned to the existing dark-paper palette and Fraunces heading font. Updates the contents nav from 6 → 7 columns. di-explainer.html gets a new §08 "Instrumentation symbols" with cards for TRACER and LOGGER, the bindAll() Rule-0 wiring path, and a note on why feature containers also bind the symbols. Two-column grid collapses to one on narrow viewports. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/architecture/data-flow-explainer.html | 117 ++++++++++++++++++++- docs/architecture/di-explainer.html | 68 +++++++++++- 2 files changed, 180 insertions(+), 5 deletions(-) diff --git a/docs/architecture/data-flow-explainer.html b/docs/architecture/data-flow-explainer.html index 306e520..8f6752c 100644 --- a/docs/architecture/data-flow-explainer.html +++ b/docs/architecture/data-flow-explainer.html @@ -1401,6 +1401,53 @@ footer .colophon { @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.32s ease; } +/* ─── §06 Tracing & error capture (Plan 10) ─────────────────────────── */ + +.trace-h3 { + font-family: "Fraunces", serif; + font-weight: 500; + font-size: 1.1rem; + margin: 32px 0 14px; + color: var(--paper); +} +.trace-p { + margin: 0 0 18px; + max-width: 70ch; +} +.trace-tree { + background: var(--ink-2, rgba(0,0,0,.32)); + padding: 1.25rem 1.5rem; + border-radius: 4px; + overflow-x: auto; + font-family: "JetBrains Mono", monospace; + font-size: 0.85rem; + line-height: 1.6; + margin: 0 0 18px; +} +.capture-rules { + border-collapse: collapse; + width: 100%; + margin: 0 0 28px; +} +.capture-rules th, .capture-rules td { + padding: 0.6rem 0.85rem; + text-align: left; + border-bottom: 1px solid var(--rule, rgba(255,255,255,.12)); +} +.capture-rules th { + font-family: "Fraunces", serif; + font-weight: 500; +} +.pii-rules { + display: grid; + gap: 0.5rem; + padding-left: 1.2rem; + margin: 0 0 14px; +} +.pii-rules li { + font-size: 0.95rem; +} + /* ─── Responsive (basic) ─────────────────────────────────────────────── */ @media (max-width: 1100px) { @@ -1427,13 +1474,14 @@ footer .colophon { @@ -2305,10 +2353,71 @@ footer .colophon { - -
+ +
§ 06
+
+

Tracing & error capture.

+

Every request produces a nested span tree: tRPC procedure → controller → use case → repository → Payload op. Errors are captured at the throw site closest to the cause, never at the boundary that translates them.

+
+
+ +

The trace tree (one tRPC request)

+
HTTP transaction               (auto, @sentry/nextjs)
+└── tRPC procedure span        (auto, sentry trpc integration)
+    └── controller span         (op="controller", DI-wrapped)
+        └── use-case span       (op="use-case", DI-wrapped)
+            └── repository span (op="repository", explicit startSpan)
+                └── Payload Local API call (auto, @sentry/node http)
+ +

Capture rules (where captureException fires)

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
LayerCapturesDoesn't capture
RepositoryInfra / Payload errors that originate hereBubbled errors
Use caseBusiness-rule violations originated in this bodyErrors from repos (already captured)
ControllerInputParseError from safeParse failureAnything else
defineErrorMiddlewareNothing — maps domain → TRPCError only
+ +

Double-report guard

+

Every error captured by SentryLogger.captureException gets a non-enumerable __sentryReported = true property. A second capture call for the same error returns early. This means each error surfaces in Sentry exactly once, regardless of how many layers it passes through.

+ +

PII rules (R31–R38, non-negotiable)

+
    +
  • sendDefaultPii: false — every Sentry.init(). CI grep gate.
  • +
  • Replay default-masks all text + inputs + media. Allowlist starts empty.
  • +
  • beforeSend scrubber strips email / password / token / cookie / authorization / ipaddress keys (substring match).
  • +
  • beforeSendTransaction scrubber strips PII query params from URLs.
  • +
  • setUser accepts only { id }. Stripping wrapper warns in dev when other keys passed.
  • +
  • IPv4/IPv6 in event payload string values redacted to [redacted-ip].
  • +
+
+ + +
+
+
§ 07

Do we need them?

diff --git a/docs/architecture/di-explainer.html b/docs/architecture/di-explainer.html index 2f8c80e..48c2799 100644 --- a/docs/architecture/di-explainer.html +++ b/docs/architecture/di-explainer.html @@ -641,11 +641,43 @@ footer .colophon { font-size: 10.5px; color: var(--ink-3); letter-spacing: 0.04em; } +/* ─── §08 Instrumentation symbols (Plan 10) ──────────────────────── */ + +.instrumentation-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 36px; + margin-top: 24px; +} +.trace-h3 { + font-family: "Fraunces", serif; + font-weight: 500; + font-size: 1.05rem; + margin: 24px 0 12px; + color: var(--paper); +} +.trace-h3:first-child { margin-top: 0; } +.trace-p { + margin: 0 0 16px; + max-width: 60ch; + font-size: 0.95rem; +} +.trace-tree { + background: var(--ink-2, rgba(0,0,0,.32)); + padding: 1rem 1.25rem; + border-radius: 4px; + overflow-x: auto; + font-family: "JetBrains Mono", monospace; + font-size: 0.82rem; + line-height: 1.55; + margin: 0 0 18px; +} + /* ─── Responsive ─────────────────────────────────────────────────── */ @media (max-width: 1100px) { .frame { padding: 0 32px; } - .cast-grid, .bindings-grid, .modes-state-grid, .bypass-grid { grid-template-columns: 1fr; } + .cast-grid, .bindings-grid, .modes-state-grid, .bypass-grid, .instrumentation-grid { grid-template-columns: 1fr; } ol.contents { grid-template-columns: repeat(2, 1fr); } .title-block { grid-template-columns: 1fr; gap: 32px; } .conditions-head, .conditions-row { grid-template-columns: 1fr; gap: 4px; } @@ -673,6 +705,7 @@ footer .colophon {
  • 05Three modes
  • 06Conditions
  • 07Tests bypass
  • +
  • 08Instrumentation symbols
  • @@ -1126,6 +1159,39 @@ footer .colophon {
    + +
    +
    +
    § 08
    +
    +

    Instrumentation symbols.

    +

    Plan 10 added two new symbols to the per-feature container — TRACER and LOGGER — bound by a separate Rule 0 in bindAll() that's orthogonal to the repo binding mode. The DSN env var decides Sentry vs Noop; USE_DEV_SEED / NODE_ENV decide real vs mock repos.

    +
    +
    + +
    +
    +

    INSTRUMENTATION_SYMBOLS.TRACER

    +

    Bound by either bindNoopInstrumentation or bindSentryInstrumentation to NoopTracer or SentryTracer. Decided by Rule 0: DSN env present → Sentry; otherwise Noop.

    + +

    INSTRUMENTATION_SYMBOLS.LOGGER

    +

    Same rule, same lifecycle. NoopLogger in the absence of a DSN; SentryLogger when DSN is set. The Sentry adapter applies the __sentryReported double-report guard internally — call sites don't manage the flag.

    +
    + +
    +

    Wiring path

    +
    bindAll()
    +  └─ resolveInstrumentation()         ← Rule 0 (DSN check)
    +       └─ Noop or Sentry binders      ← bind to sharedContainer
    +  └─ bindProductionX(config, tracer, logger)
    +       └─ feature container also binds TRACER + LOGGER
    +       └─ withSpan(tracer, ...) at every use case + controller
    + +

    Why per-feature containers also get the binding: repository classes resolve TRACER/LOGGER through the container; controllers and use cases receive instrumentation via the bind-time wrapper instead.

    +
    +
    +
    +