The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.0 KiB
VD-07: Diagnostics와 telemetry exporter 경계
- 상태: Accepted
- 결정일: 2026-07-26
- 적용 브랜치:
feature-frontend-diagnostics-telemetry-runtime - 재검토: 실제 운영 sink, consent가 필요한 analytics 또는 분산 tracing provider가 선정될 때
배경
기존 telemetry registry와 best-effort HTTP queue는 있었지만 운영 진단 record와 semantic event의 책임이 하나의 telemetry port에 섞여 있었다. boot, HTTP, cache, storage, route와 release failure의 선언도 실제 production producer와 완전히 연결되지 않았다. 이 상태에서는 retry attempt마다 같은 사건을 발행하거나 raw URL, query, request body와 오류 객체가 queue에 들어갈 위험이 있다.
반면 skeleton 단계에는 실제 관측 vendor, endpoint의 운영 보안 정책, analytics consent와 보존 기간이 결정되지 않았다. 특정 SDK를 기본 번들에 설치하는 것은 vendor 결정 전에는 안전한 기본값이 아니다.
결정
- level 기반 운영 진단은
DiagnosticsPort, registry 기반 semantic event는TelemetryPort로 분리한다. application은 두 port의 concrete adapter나 exporter SDK를 알지 못한다. - diagnostics의 level, event ID와 context key는 닫힌 registry/allowlist다. telemetry도 event별 required/optional attribute와 value policy를 적용한다. 등록되지 않은 event·context·고카디널리티 값은 전송하지 않는다.
- 기본 diagnostics adapter는 bounded in-memory evidence이고 telemetry는 설정이 없으면 true no-op이다. endpoint가 있을 때만 bounded oldest-drop queue와 best-effort HTTP sink를 사용한다.
- raw path/URL/query/body/response/storage value, credential, cookie, email, stack과 오류 객체 전체는 context에 넣지 않는다. route ID, operation ID, correlation ID, release ID, error kind, status/attempt/duration bucket만 허용한다.
- HTTP logical execution은 success, retry recovery, terminal failure 또는
abort마다
http.request.completeddiagnostics를 정확히 한 번 남긴다.api.request.failedtelemetry는 retry가 끝난 terminal non-abort failure에만 정확히 한 번 발행한다. 5-1. V2 client와 V3 contract executor는 각각 자신의 logical execution에 대해 이 규칙을 만족한다. V3에서는 execution site가HttpExecutionObservationtyped record 하나만 만들고, composition root의createHttpObservationProjector가 유일한 projection authority다. observation은 arbitrary context map이 아니며 projector는route_id,operation_id,operation,outcome,error_kind,http_status_group,attempt_count_bucket,duration_bucket만 사용한다. raw attempt count, duration, status, URL, intent, key, input identity와 내부terminalReason은 sink로 나가지 않는다. effect certainty가 운영상 필요해지면effect_certaintykey와 닫힌 value policy를 contract·fixture·이 ADR에 동시에 추가한 뒤에만 전달한다. 5-2. caller cancellation과 scope fence는 API failure가 아니다. diagnostics는 한 번 남기고api.request.failed는 발행하지 않는다. 5-3.routeId는 installed operation-executor 경계의 필수 입력이다. feature gateway가 소유한 low-cardinality route identity를 URL에서 재구성하지 않는다. app.boot.failed,ui.render.failed,release.mismatch.detected,telemetry.delivery.dropped를 production path에 연결한다. cache와 storage 실패는 diagnostics로 기록하되 raw key/value를 기록하지 않는다.- queue full, invalid event/context, serialization과 sink failure는 제한된
reason bucket으로 집계한다. drop observer의 failure는 다시 telemetry를
발행하지 않는 nonrecursive 경계다.
7-1. telemetry adapter lifecycle은
ACTIVE | DISPOSED둘뿐이다.dispose()는 한 번만 전이하고pagehidelistener 제거, queue 비우기, scheduled callback generation 무효화, in-flight sinkAbortControllerabort를 모두 수행한다. dispose 뒤emit()은 no-op이고 새 flush는 스케줄되지 않으며, abort를 무시한 sink가 늦게 settle해도 post-dispose delivery state를 갱신하거나 재스케줄하지 못한다. 종료 중 drop telemetry를 재귀적으로 발행하지 않는다. 7-2.flush()는 active delivery promise를 join한다. 이미 진행 중인 flush가 있으면 같은 promise를 반환하므로await flush()는 실제 settle을 뜻한다. 7-3. runtimeinfrastructure.dispose()는 diagnostics/state dependency를 파괴하기 전에telemetry.dispose()를 먼저 호출한다. 7-4. queue/entry capacity는 construction-time 계약이다.Number.isSafeInteger가 아니거나 1 미만이거나 문서화된 ceiling(각각MAX_TELEMETRY_QUEUE,MAX_DIAGNOSTIC_ENTRIES= 10,000)을 넘으면TypeError로 거절한다. NaN/Infinity가 조용히 eviction을 비활성화하는 경로를 남기지 않는다. - diagnostics와 telemetry failure는 제품 흐름, HTTP 결과, route transition, storage/cache fallback과 React error surface를 바꾸지 않는다.
- mount 전 bootstrap failure는 안전한 build/config/error kind만 별도 evidence로 만들며 untrusted error message, stack과 support 입력을 serialize하지 않는다.
- 실제 error reporter, RUM, analytics나 tracing SDK는 같은 port 뒤의 외부 adapter로만 추가한다. SDK type과 event API를 application/feature/presentation public contract에 노출하지 않는다.
실행 경계
route/application/HTTP/cache/storage/bootstrap
-> typed DiagnosticsPort 또는 TelemetryPort
-> registry + allowlist + value policy
-> bounded memory/no-op 또는 best-effort HTTP adapter
-> 프로젝트가 선택한 외부 sink
- diagnostics contract:
src/contracts/diagnostics.ts - telemetry contract:
src/contracts/telemetry.ts - application ports:
src/application/ports/diagnostics-port.ts,telemetry-port.ts - bounded diagnostics:
src/adapters/diagnostics/bounded-diagnostics.ts - best-effort telemetry:
src/adapters/telemetry/best-effort-telemetry.ts - composition:
src/bootstrap/runtime-adapters.ts
검증
check:diagnostics는 모든 registry event에 production producer가 있는지와 source의 direct console/sensitive context 우회를 검사한다.- negative source fixture는 direct console, unknown event와 raw context를 실제로 거절하며 TypeScript fixture는 잘못된 level/event ID를 거절한다.
- unit test는 allowlist, hostile/circular error, bounded diagnostics, no-op, queue full, sink/observer failure와 pre-mount boot evidence를 검증한다.
- HTTP integration은 success, retry recovery, terminal failure와 abort의 producer 횟수, route/operation/correlation context와 요청 값 비노출을 검증한다.
tests/integration/http-execution-v3-observability.test.ts는 V3 terminal outcome이 실제로 closed allowlist를 통과하는지, terminal non-abort failure가api.request.failed를 정확히 한 번 발행하는지, cancellation/scope fence가 발행하지 않는지, feature route ID가 executor 경계까지 보존되는지, sink 예외가 HTTP 결과를 바꾸지 못하는지를 검증한다.- cache/storage/release/application/runtime test는 각 production wiring과 diagnostics failure isolation을 검증한다.
한계와 재검토 조건
기본 adapter는 운영 log 검색, source map 연계, session replay, distributed span, analytics consent, sampling budget과 장기 보존을 제공하지 않는다. 실제 sink를 선정할 때 데이터 처리 지역, 보존 기간, consent, CSP, source map 접근 제어, sampling과 비용 상한을 별도 결정해야 한다.
Rollback
telemetry exporter는 runtime 설정을 끄거나 adapter wiring을 noOpTelemetry로
바꾸어 독립적으로 제거할 수 있다. 이때도 DiagnosticsPort, registry,
redaction/value policy, producer-count와 negative fixture는 유지한다. 외부 SDK
문제로 application producer와 안전 계약을 함께 되돌리지 않는다.