docs(html): update §06 + §08 with verified layer-by-layer instrumentation usage
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
<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", 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)</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>
|
||||
<table class="capture-rules instrumentation-where">
|
||||
<thead>
|
||||
<tr><th>Layer</th><th>Span</th><th>Capture</th><th>How</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Use case body</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
<td>Composed: <code>withSpan(withCapture(useCase(deps)))</code> in <code>bind-production.ts</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Controller body</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
<td>Composed: <code>withSpan(withCapture(controller(uc)))</code> in <code>bind-production.ts</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repository (real)</td>
|
||||
<td><strong>Inline</strong> per method</td>
|
||||
<td><strong>Inline</strong> in <code>catch</code></td>
|
||||
<td><code>this.tracer.startSpan(...)</code> + <code>this.logger.captureException(...)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repository (mock)</td>
|
||||
<td><strong>Inline</strong> per method</td>
|
||||
<td>—</td>
|
||||
<td>Span shape parity with real; mocks don't originate infra errors</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>tRPC procedure</td>
|
||||
<td>Auto (SDK)</td>
|
||||
<td>—</td>
|
||||
<td>Sentry's tRPC integration — no code in this repo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>defineErrorMiddleware</code></td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
<td>Maps domain errors → TRPCError. Never captures (R44 boundary)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="trace-p"><strong>Verifiable:</strong> <code>grep -rn "this.tracer\|this.logger" packages/*/src</code> returns hits only in <code>infrastructure/repositories/*.repository.ts</code> and <code>*.repository.mock.ts</code>. Use case and controller bodies have zero matches. <code>withSpan</code> / <code>withCapture</code> appear only in <code>di/bind-*.ts</code> files.</p>
|
||||
|
||||
<h3 class="trace-h3">The wrapper sandwich (one feature, in <code>bind-production.ts</code>)</h3>
|
||||
<pre class="trace-tree"><code>// 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),
|
||||
),
|
||||
);</code></pre>
|
||||
<p class="trace-p"><strong>Order matters.</strong> <code>withSpan</code> is outermost so the errored span's timing reflects the captured-and-rethrown failure. <code>withCapture</code> is between span and factory so the error is captured <em>before</em> the span closes with error status.</p>
|
||||
|
||||
<h3 class="trace-h3">Capture rules (where <code>captureException</code> fires)</h3>
|
||||
<table class="capture-rules">
|
||||
<thead>
|
||||
@@ -2380,17 +2474,17 @@ footer .colophon {
|
||||
<tr>
|
||||
<td>Repository</td>
|
||||
<td>Infra / Payload errors that originate here</td>
|
||||
<td>Bubbled errors</td>
|
||||
<td>Bubbled errors (already captured downstream)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Use case</td>
|
||||
<td>Business-rule violations originated in this body</td>
|
||||
<td>Errors from repos (already captured)</td>
|
||||
<td>Business-rule violations originated in this body (e.g. <code>AuthenticationError</code>) and output-schema validation failures</td>
|
||||
<td>Errors from repos — flag is set, <code>withCapture</code> bails</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Controller</td>
|
||||
<td><code>InputParseError</code> from safeParse failure</td>
|
||||
<td>Anything else</td>
|
||||
<td><code>InputParseError</code> from <code>safeParse</code> failure</td>
|
||||
<td>Errors from use cases — flag is set, <code>withCapture</code> bails</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>defineErrorMiddleware</code></td>
|
||||
@@ -2401,7 +2495,7 @@ footer .colophon {
|
||||
</table>
|
||||
|
||||
<h3 class="trace-h3">Double-report guard</h3>
|
||||
<p class="trace-p">Every error captured by <code>SentryLogger.captureException</code> gets a non-enumerable <code>__sentryReported = true</code> 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.</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>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>
|
||||
|
||||
<h3 class="trace-h3">PII rules (R31–R38, non-negotiable)</h3>
|
||||
<ul class="pii-rules">
|
||||
|
||||
Reference in New Issue
Block a user