From 01707e801b1c64e86520ac37702939789aaa0c22 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 8 May 2026 00:43:42 +0200 Subject: [PATCH] =?UTF-8?q?docs(html):=20update=20=C2=A706=20+=20=C2=A708?= =?UTF-8?q?=20with=20verified=20layer-by-layer=20instrumentation=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the user pushed back on intent-vs-shipped state, I greped each layer and updated both explainers to reflect actual code, not documented intent. data-flow-explainer.html §06 — "Tracing & error capture": - New "Where instrumentation actually lives" table — per-layer breakdown of inline span / inline capture / composed-via-wrapper, with a verifiable grep showing only repos have inline calls. - New "The wrapper sandwich" code block showing the actual repo body shape next to the bind-time withSpan(withCapture(...)) composition. - Capture-rules table refined to reflect the R44 fix that just landed: use cases capture business-rule errors and output-schema failures (not bubbled-from-repo); controllers capture safeParse failures (not bubbled-from-use-case); the __sentryReported flag is what makes this safe. - Double-report-guard paragraph now mentions withCapture, SentryLogger, and RecordingLogger all check the flag (not just SentryLogger). di-explainer.html §08 — "Instrumentation symbols": - Wiring path updated from "withSpan(tracer, ...)" to "withSpan(withCapture(...))" to reflect the post-merge wiring. - New "Two wrappers, applied as a sandwich" table comparing what each wrapper does and where it fires; closing note that repos can't use the wrapper because they own per-call attributes. Also bundled: a 1-line aesthetic SVG noise tweak in di-explainer.html (opacity='0.25', baseFrequency 0.85→0.95) that was sitting in the working tree before this session — preserved across the Plan 10 merge via stash/pop and now committed alongside the doc update. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/architecture/data-flow-explainer.html | 112 +++++++++++++++++++-- docs/architecture/di-explainer.html | 25 ++++- 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/docs/architecture/data-flow-explainer.html b/docs/architecture/data-flow-explainer.html index 8f6752c..fd42fc3 100644 --- a/docs/architecture/data-flow-explainer.html +++ b/docs/architecture/data-flow-explainer.html @@ -1438,6 +1438,13 @@ footer .colophon { font-family: "Fraunces", serif; font-weight: 500; } +.capture-rules.instrumentation-where th:nth-child(2), +.capture-rules.instrumentation-where th:nth-child(3), +.capture-rules.instrumentation-where td:nth-child(2), +.capture-rules.instrumentation-where td:nth-child(3) { + white-space: nowrap; + width: 1%; +} .pii-rules { display: grid; gap: 0.5rem; @@ -2366,11 +2373,98 @@ footer .colophon {

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)
+    └── 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)
+

Where instrumentation actually lives

+

Two ways spans + captures get attached. Inline means the call appears in the layer's own body. Composed-in means a higher-order wrapper applied at DI bind time — the body stays vendor-clean.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LayerSpanCaptureHow
Use case bodyComposed: withSpan(withCapture(useCase(deps))) in bind-production.ts
Controller bodyComposed: withSpan(withCapture(controller(uc))) in bind-production.ts
Repository (real)Inline per methodInline in catchthis.tracer.startSpan(...) + this.logger.captureException(...)
Repository (mock)Inline per methodSpan shape parity with real; mocks don't originate infra errors
tRPC procedureAuto (SDK)Sentry's tRPC integration — no code in this repo
defineErrorMiddlewareMaps domain errors → TRPCError. Never captures (R44 boundary)
+

Verifiable: grep -rn "this.tracer\|this.logger" packages/*/src returns hits only in infrastructure/repositories/*.repository.ts and *.repository.mock.ts. Use case and controller bodies have zero matches. withSpan / withCapture appear only in di/bind-*.ts files.

+ +

The wrapper sandwich (one feature, in bind-production.ts)

+
// Repository — inline, per public method
+class ArticlesRepository {
+  async getArticles(input) {
+    return this.tracer.startSpan(
+      { name: "articles.getArticles", op: "repository", attributes: { /* ... */ } },
+      async (span) => {
+        try {
+          const result = await /* payload op */;
+          span.setAttribute("count", result.length);
+          return result;
+        } catch (err) {
+          this.logger.captureException(err, {
+            tags: { feature: "blog", repo: "articles", method: "getArticles" },
+          });
+          span.setStatus("error", String(err));
+          throw err;
+        }
+      },
+    );
+  }
+}
+
+// Use cases + controllers — composed at bind time, body stays clean
+const wrappedUC = withSpan(
+  tracer, { name: "blog.getArticles", op: "use-case" },
+  withCapture(
+    logger, { feature: "blog", layer: "use-case", name: "blog.getArticles" },
+    getArticlesUseCase(repo),
+  ),
+);
+const wrappedCtrl = withSpan(
+  tracer, { name: "blog.getArticles", op: "controller" },
+  withCapture(
+    logger, { feature: "blog", layer: "controller", name: "blog.getArticles" },
+    getArticlesController(wrappedUC),
+  ),
+);
+

Order matters. withSpan is outermost so the errored span's timing reflects the captured-and-rethrown failure. withCapture is between span and factory so the error is captured before the span closes with error status.

+

Capture rules (where captureException fires)

@@ -2380,17 +2474,17 @@ footer .colophon { - + - - + + - - + + @@ -2401,7 +2495,7 @@ footer .colophon {
Repository Infra / Payload errors that originate hereBubbled errorsBubbled errors (already captured downstream)
Use caseBusiness-rule violations originated in this bodyErrors from repos (already captured)Business-rule violations originated in this body (e.g. AuthenticationError) and output-schema validation failuresErrors from repos — flag is set, withCapture bails
ControllerInputParseError from safeParse failureAnything elseInputParseError from safeParse failureErrors from use cases — flag is set, withCapture bails
defineErrorMiddleware

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.

+

Each error gets a non-enumerable __sentryReported flag the first time it's captured. withCapture, SentryLogger, and RecordingLogger all check the flag and bail if it's set. So an error bubbling repo → use-case → controller surfaces in the logger exactly once, with the inner-most layer's tags. Helper lives in core-shared/instrumentation/reported-flag.ts.

PII rules (R31–R38, non-negotiable)