성능·보존 진단 규격 — 열쇠·채널·금지 필드 (이슈 #1284, G04)
한 문장 — 유저 A 의 저장 한 건이 브라우저·기기 대기열·서버 잡·통계 세대·릴리스를 지나는 동안 남기는 흔적을 같은 열쇠로 잇는다. 어느 계측이 어느 열쇠를 싣는지, 무엇을 절대 싣지 않는지를 한 곳에 적는다. 새 계측을 켜지 않고 기존 계측에 열쇠와 모양만 맞춘다(측정 경로 비용 제한).
- 이슈: #1284 (계획 ID G04). 선행 계약: G03 도메인 계약 §8(owner 세대)·§9(통계 세대)·§13(영수증)·§15(저장 단계). 현행 로그 규칙: 관측·로깅.
- 코드:
src/react/services/performanceEvidenceReport.ts(PERFORMANCE_EVIDENCE_SCHEMA_VERSION = 1, 증거 JSON 조립·위생 검사) ·src/react/services/appErrorReporter.tsresolveReleaseIdentity(릴리스 식별). - 검사:
tests/react/performanceEvidenceReport.test.mjs(열쇠·위생·릴리스 식별). - 형제 문서: 기준선(기준 SHA 의 숫자) · 릴리스 예산(상한과 추세).
1. 잇는 열쇠 (evidence key)
| 열쇠 | 뜻 | 어디서 나오나 | 어느 계측에 실리나 |
|---|---|---|---|
release | 이 번들·서버 상태를 만든 커밋 SHA(7~40 hex). 주입 전에는 앱 청크 파일 이름(app-XXXX.js) | 빌드가 globalThis.__BARBELIC_RELEASE__ 로 주입(§2) | 오류 이벤트 release, 증거 JSON release, 서버 프로브 release(HEAD SHA) |
operationId | 한 작업의 식별자(rpc:<이름>:…) | barbelicRepository.createOperationId | RPC 디버그 행, 오류 이벤트 operation_id |
clientMutationId | 쓰기 명령의 멱등 키(uuid) — 계약 §3 | 앱(durableMutationIdentity) | 대기열 행, 영수증 client_mutation_id, 쓰기 실패 오류 이벤트의 operation_id(규칙 §4) |
jobId | 통계 잡 id(uuid) | 서버 user_exercise_stats_refresh_jobs.id | 잡 행, 관측 뷰 job_id |
generation | 통계 세대 {requested, applied} — 계약 §9 | 영수증 stats_requested_version / 읽기 모델 freshness | 영수증, 읽기 모델, user_stats_refresh_state |
ownerHash | 사용자 id 의 SHA-256 앞 16 hex — 원문 id 대신 | 서버 프로브가 계산(encode(digest(user_id::text,'sha256'),'hex')) | 서버 프로브 집계행. 브라우저 증거는 비운다(사용자 한 명의 기기이므로 필요 없다) |
at | 절대 시각(ISO, Z/오프셋) — 계약 §4 | 각 계측의 시계 | 전부 |
한 저장 건의 사슬: clientMutationId → 영수증(committed_at, stats_requested_version) → 잡(source_ref·target_version) → user_stats_refresh_state.applied_version/applied_at → 화면 읽기 모델 freshness. 이 사슬로 "저장 → 서버 확정 → 통계 반영" 세 시각을 잇는다(계약 §15).
2. 릴리스 식별
- 정본 = 커밋 SHA. 빌드가
globalThis.__BARBELIC_RELEASE__ = "<sha>"를 주입하면resolveReleaseIdentity가 그것을 쓴다(소문자 7~40 hex 만 인정). 주입은vite.config.mjs의define한 줄(VERCEL_GIT_COMMIT_SHA→GITHUB_SHA→"dev")이며 HQ 통합 슬롯(B02) 이 넣는다 — G04 갱신안 항목. - 주입 전에는 종전대로 앱 청크 파일 이름이다(2026-09-07 실측: 30일 동안 58종). 청크 이름과 SHA 의 대응은 Vercel 배포 기록에서만 알 수 있으므로 비교 자료로 쓰지 않는다.
- 서버 쪽 릴리스 = Production 프로브가 기록하는
git rev-parse HEAD(main) +deploymentManifest.EXPECTED_LATEST_MIGRATION. 앱과 서버는 릴리스 PR 로 함께 나가므로 같은 SHA 다.
3. 금지 필드 (telemetry 위생)
증거·오류 이벤트·프로브 집계 어디에도 기록 본문·메모·제목·인입 원문·인증 토큰·이메일·사용자 id 원문 을 싣지 않는다. 검사는 필드 이름으로 한다(값을 보고 판단하지 않는다 — 값이 비어 있어도 이름이 걸리면 실패).
EVIDENCE_FORBIDDEN_FIELD_NAMES(정규화: 대소문자·_·- 무시): note(s)·memo·title·review·text·body·comment·message / payload·raw(Payload)·request·response·params·rows·record·session·exercises·sets / token·accessToken·refreshToken·authorization·apiKey·secret·password·cookie·jwt / email·phone·displayName·handle·avatarUrl·userId·user·owner.
현행 오류 채널 record_client_error_event 는 allowlist 21키에 sanitizeDiagnosticText(토큰·이메일·제목 치환)를 이미 갖췄다 — 이 규격은 그것을 바꾸지 않고, 증거 JSON 과 프로브 출력에 같은 기준을 적용한다. readModel 증거의 key 는 ResourceKey 문자열의 종류 조각만 남긴다(owner id 를 담을 수 있으므로).
4. 채널 대응표 — 지금 있는 계측이 무엇을 싣고, 규격이 무엇을 더하나
| 채널 | 어디에 남나 | 지금 싣는 것 | 규격이 정하는 것 | 담당 |
|---|---|---|---|---|
RPC 디버그(recordScreenRpcDebug) | sessionStorage["barbelic-rpc-debug-log"]·globalThis.__liftGuildRpcDebug(120건) | at·event·operationName·rpcName·durationMs·responseBytes·status·errorCode·operationId·paramKeys | 증거 JSON rpc[] 로 내보낸다(paramKeys 제외). 서버 업로드 없음 | G04(내보내기) |
read-model 메트릭(recordReadModelMetric) | 메모리 200건 | kind·scope·key·durationMs | 증거 readModel[](key 는 종류만) | G04 |
| Navigation/Resource Timing | 브라우저 API | 표준 구간 | 증거 navigation(dns·connect·request·response·domInteractive·DCL·load·transferBytes·cold/warm)·resources(script/style/font/other 개수·바이트·가장 느린 script)·dom.nodeCount | G04 |
오류 이벤트(record_client_error_event) | client_error_events(90일) | allowlist 21키, release·operation_id·duration_ms, kind 에 perf_tab_switch | release 는 §2 resolver. 쓰기 실패(sync_failure·rpc_failure of save_session_v5/delete_session_v5)의 operation_id 는 clientMutationId — 대기열 행·영수증과 잇기 위해. 새 성능 kind 추가는 SQL CHECK 변경(마이그레이션) | 규칙 = G04 · 적용 = S05(전송기)·S11 · kind 추가 = D 계열 |
| 대기열 행 | IndexedDB pendingSaves | clientMutationId·savedAt·state·sentAt… | 큐 age = now − savedAt(queued)·전송 중 age = now − sentAt. 기기에서 집계해 증거에 싣는 것은 S05/S11(지금 미측정) | S05·S11 |
| 영수증 | workout_mutation_receipts | client_mutation_id·committed_at·stats_requested_version·server_revision | 서버 확정 시각의 정본. 클라 전송 시각은 서버에 없다 — "요청 → 확정" 은 기기 증거(sentAt)와 영수증(committed_at)을 clientMutationId 로 이어 계산 | G04(프로브) |
| 잡 행·관측 뷰 | user_exercise_stats_refresh_jobs·…_observability | created_at·metadata.processing_started_at·completed_at·duration_ms(파생) | 큐 age = processing_started_at − created_at, 처리 = completed_at − processing_started_at. 결함: 두 시각이 같은 트랜잭션 now() 라 처리 시간이 항상 0 — clock_timestamp() 로 바꾸는 것은 D08 | D08 |
| 세대 상태 | user_stats_refresh_state | requested_at·applied_at·두 버전 | freshness 지연 = applied_at − requested_at, stale = requested > applied | G04(프로브)·D11 |
| cron 이력 | cron.job_run_details | start_time·end_time·status | 잡별 실행 시간 분포·5초 초과 횟수. 보존 정리 없음(7/20~ 127k행) — 정리는 D08 | G04(프로브) |
| Edge 함수 | console | duration_ms | 로그 전용(Supabase 로그 API). 증거로 모으지 않는다(호출 빈도 낮음) | — |
| DB 통계 | pg_stat_wal·pg_stat_user_tables·pg_stat_database·extensions.pg_stat_statements | 누적값 | 프로브가 두 시점의 차로 WAL·행·캐시 적중률을 낸다. pg_stat_statements 는 PostgREST 래핑 때문에 RPC 이름이 안 보인다 — 함수별 시간은 track_functions = pl(DB 설정, D08/HQ) 뒤에만 | G04(프로브)·D08 |
5. 증거 JSON (schema v1)
PerformanceEvidenceReport {
schemaVersion: 1, release, collectedAt, userAgent,
rpc[]: { kind:"rpc", release, at, operationId, event, rpcName, durationMs, responseBytes, status, errorCode }
readModel[]: { kind:"read_model", release, at, metric, scope, key(종류만), durationMs }
navigation: { kind:"navigation", dnsMs, connectMs, requestMs, responseMs, domInteractiveMs, domContentLoadedMs, loadEventMs, transferBytes, cacheState } | null
resources: { kind:"resources", scriptCount, scriptBytes, styleCount, styleBytes, fontCount, fontBytes, otherCount, otherBytes, slowestScriptMs } | null
dom: { kind:"dom", nodeCount } | null
}- 값이 없으면
null(미측정).0은 "잰 결과 0" 이다 — 둘을 섞지 않는다(완료 조건: 누락 계측은 미측정으로 보고). buildPerformanceEvidence는 조립 뒤assertEvidenceHygiene를 통과해야 결과를 돌려준다. 통과 못 하면 증거를 만들지 않는다.- 서버 프로브 출력(
scripts/performance/probe)은 같은 열쇠 이름과 같은 금지 목록을 쓴다(schema.json, Phase 3).
6. 측정 경로 자체의 비용
- 브라우저: 새 타이머·observer 를 켜지 않는다.
collectPerformanceEvidence는 러너가 부를 때 한 번 읽는다.perf_tab_switch프로브의 기존 상한(세션당 20건·5초 간격)은 그대로. - 서버: 프로브는 읽기 전용·집계 쿼리만, Production 은 Management API
read_only: true. 매분 cron 이 이미 쓰는 비용 위에 새 쓰기를 더하지 않는다. - 저장: 증거 JSON 은 파일(
scripts/performance/evidence/<sha>/…)이며 저장소에는 기준 SHA 결과만 문서 표로 남긴다.