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 {
Contents
-
    +
    1. 01Feature anatomy
    2. 02Request flow
    3. 03Dependency injection
    4. 04Mocks, contracts & factories
    5. 05Tradeoffs by part
    6. -
    7. 06The verdict
    8. +
    9. 06Tracing & error capture
    10. +
    11. 07The verdict
@@ -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)

+ +
+ + +
+
+
§ 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.

    +
    +
    +
    +