제로 의존성 분석 API, 이중 파이프라인 라우팅, Datadog, 메타데이터 강화, PII 위생, GrowthBook 기능 플래그.
Claude Code의 분석 시스템은 제로 의존성 원칙을 따르는 경량 텔레메트리 아키텍처입니다. 외부 분석 SDK 없이 자체 구현된 이벤트 수집, 배치 전송, 메타데이터 강화, PII 위생 처리를 제공합니다. 6개 파일로 구성된 모듈러 아키텍처가 이벤트의 전체 수명 주기를 관리합니다.
analytics/ 디렉토리 — index.ts(공개 API + 큐), sink.ts(라우터), datadog.ts(Datadog 전송), metadata.ts(메타데이터 강화), growthbook.ts(기능 플래그), sinkKillswitch.ts(긴급 오프 스위치)
6파일 아키텍처의 책임 분리:
index.ts: 공개 API — logEvent(), attachAnalyticsSink() 등 진입점 함수와 프리싱크 큐sink.ts: 싱크 라우터 — _PROTO_ 접두사 키에 따라 1P 전용 BigQuery 컬럼으로 라우팅하고 Datadog에 병렬 디스패치datadog.ts: Datadog 전송 — 약 40개 허용 이벤트 큐레이션, 배치 플러시, 카디널리티 감소metadata.ts: 메타데이터 강화 — EnvContext, ProcessMetrics, 에이전트 식별growthbook.ts: GrowthBook 기능 플래그 — 원격 평가, 3레벨 오버라이드, 노출 중복 제거sinkKillswitch.ts: 킬스위치 — tengu_frond_boric 키로 Datadog과 firstParty 싱크를 독립적으로 비활성화logEvent()가 싱크 연결 전에 호출되면 이벤트가 인메모리 eventQueue에 푸시됩니다. attachAnalyticsSink()로 싱크가 연결되면 queueMicrotask로 대기열을 드레인합니다 — 시작 절차에 동기적 지연을 추가하지 않습니다. attachAnalyticsSink()는 멱등성 가드가 있어 중복 호출 시 두 번째 싱크를 무시합니다.
const eventQueue: QueuedEvent[] = []
let sink: AnalyticsSink | null = null
export function logEvent(eventName: string, metadata: LogEventMetadata): void {
if (sink === null) {
eventQueue.push({ eventName, metadata, async: false })
return
}
sink.logEvent(eventName, metadata)
}
export function attachAnalyticsSink(newSink: AnalyticsSink): void {
if (sink !== null) return // 멱등
sink = newSink
if (eventQueue.length > 0) {
const queued = [...eventQueue]
eventQueue.length = 0
queueMicrotask(() => {
for (const e of queued) sink!.logEvent(e.eventName, e.metadata)
})
}
}
queueMicrotask는 현재 마이크로태스크 큐 끝에 드레인 작업을 스케줄링합니다. setTimeout(0)과 달리 매크로태스크 큐를 사용하지 않으므로 I/O 콜백 이전에 실행됩니다. 이렇게 하면 attachAnalyticsSink() 호출자의 동기 흐름에 지연을 추가하지 않으면서도 대기 중인 이벤트가 빠르게 드레인됩니다.
이벤트 메타데이터에서 _PROTO_ 접두사가 붙은 키는 1P(First-Party) 전용 BigQuery 컬럼으로 라우팅됩니다. Datadog으로 전송하기 전에 stripProtoFields()가 이러한 필드를 제거하여 내부 데이터가 외부 서비스로 유출되지 않도록 합니다.
_PROTO_ 필드 분리shouldSampleEvent() 체크 — 샘플링 비율에 따라 이벤트 드롭 또는 통과stripProtoFields()로 내부 전용 필드 제거_PROTO_ 필드 명명 규칙을 어기면 내부 전용 데이터가 Datadog으로 전송될 수 있습니다. 새로운 1P 전용 메타데이터를 추가할 때는 반드시 _PROTO_ 접두사를 사용해야 합니다.
Datadog으로의 이벤트 전송은 효율성, 비용 최적화, 프라이버시를 동시에 고려합니다.
약 40개의 허용된 이벤트만 Datadog으로 전송됩니다. 허용 목록에 없는 이벤트는 자동으로 드롭되어 불필요한 비용을 방지합니다.
이벤트는 15초 간격 또는 100개 누적 시 (먼저 도달하는 조건) 배치로 전송됩니다. 개별 HTTP 요청의 오버헤드를 줄이고 네트워크 효율성을 높입니다.
높은 카디널리티 태그(모델 이름, MCP 도구 이름 등)는 Datadog 비용을 급증시킵니다. 분석 시스템은 이러한 값을 정규화하여 카디널리티를 제한합니다:
사용자 ID는 SHA-256으로 해싱된 후 30개 버킷으로 매핑됩니다. 이 방식은 개별 사용자를 식별할 수 없으면서도 고유 사용자 수를 약 3.3% 오차로 추정할 수 있게 합니다.
// datadog.ts — 30버킷 프라이버시 보존 해싱
function hashToBucket(userId: string): number {
const hash = sha256(userId)
return parseInt(hash.slice(0, 8), 16) % 30 // ~3.3% 오차
}
모든 이벤트는 전송 전에 세 가지 범주의 메타데이터로 강화됩니다.
플랫폼, 아키텍처, 런타임 버전, CI 여부 등 환경 정보를 수집합니다. 이 정보는 세션 시작 시 한 번만 수집하고 메모이즈합니다 — 매 이벤트마다 재수집하면 불필요한 오버헤드가 발생합니다.
RSS(Resident Set Size), 힙 사용량, CPU 퍼센트를 수집합니다. EnvContext와 달리 ProcessMetrics는 이벤트별 델타로 기록됩니다 — 시간에 따른 리소스 사용량 변화를 추적합니다.
이벤트 발생 컨텍스트에 따라 에이전트를 식별합니다:
AsyncLocalStorage를 사용하여 메인 에이전트와 서브에이전트를 구분Node.js의 AsyncLocalStorage는 비동기 호출 체인 전체에 걸쳐 컨텍스트를 유지합니다. 서브에이전트가 생성될 때 고유 식별자가 AsyncLocalStorage에 저장되고, 해당 서브에이전트 내의 모든 이벤트에 자동으로 태깅됩니다. 이를 통해 메인 에이전트와 서브에이전트의 이벤트를 정확하게 분리할 수 있습니다.
개인 식별 정보(PII)와 코드/파일 경로의 분석 백엔드 유출을 방지하기 위해 컴파일 타임과 런타임 양쪽에서 보호합니다.
never 타입 마커TypeScript의 never 타입을 활용한 페이퍼 트레일 패턴입니다. 분석 메타데이터 타입 이름이 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS로 되어 있으며, 이 타입이 never(바텀 타입)이므로 값을 할당하려면 명시적 as 캐스트가 필요합니다.
// 타입 정의 — never 타입으로 명시적 캐스트 강제
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
// 사용 시 — as 캐스트가 코드 리뷰 신호 역할
const metadata = sanitizedValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
이 패턴의 의도: as 캐스트를 하려면 개발자가 "이 값이 코드나 파일 경로가 아님을 확인했다"는 타입 이름을 읽어야 합니다. 코드 리뷰어에게도 해당 캐스트의 의미를 즉시 전달합니다.
"mcp_tool"로 일반화되어 도구 이름에 포함될 수 있는 민감한 정보를 제거합니다never 타입 캐스트는 TypeScript 컴파일러 체크일 뿐 런타임 보호는 아닙니다. 런타임에서의 잘라내기와 일반화 로직이 실제 데이터 유출을 방지합니다. 두 레이어 모두 필요합니다.
GrowthBook은 기능 플래그와 A/B 테스트를 관리하는 시스템입니다. Claude Code는 원격 평가 모드를 사용합니다.
기능 플래그는 로컬에서 평가되지 않고 GrowthBook 서버에서 평가된 결과를 받아옵니다. 복잡한 타겟팅 규칙을 클라이언트에 배포하지 않아도 됩니다.
원격 평가에 사용되는 주요 사용자 속성:
deviceID — 기기 식별자platform — OS 플랫폼subscriptionType — 구독 유형기능 플래그 값은 세 단계에서 오버라이드할 수 있으며, 우선순위가 높은 순서대로 적용됩니다:
features 객체가 비어 있으면 디스크 캐시를 보존합니다. 서버 장애 시 마지막 알려진 양호한 설정을 유지합니다.tengu_frond_boric — 의도적으로 의미 없는 키 이름을 사용하여 우연한 충돌을 방지하는 킬스위치입니다.
킬스위치는 Datadog 싱크와 firstParty 싱크를 독립적으로 비활성화할 수 있습니다. 하나의 싱크에 문제가 발생했을 때 다른 싱크에 영향을 주지 않고 개별적으로 끌 수 있습니다.
킬스위치는 실패 개방(fail-open) 설계를 따릅니다:
firstParty 킬스위치가 true로 설정되면 이벤트가 삭제되지 않습니다. 대신 이벤트가 디스크에 큐잉되고 백오프 타이머가 틱합니다. 킬스위치 플래그가 해제되면 대기 중인 이벤트의 전달이 재개됩니다. 이 설계로 일시적 장애 시에도 이벤트가 유실되지 않습니다.
킬스위치 외에도 여러 조건에서 분석 시스템이 비활성화됩니다:
피드백 설문(사용자 만족도 조사 등)은 서드파티 프로바이더(Bedrock, Vertex 등)에서도 활성 상태를 유지합니다. 프로바이더에 관계없이 제품 경험 데이터를 수집하기 위함입니다.
shouldSampleEvent() 함수가 이벤트별 샘플링을 제어합니다.
null 반환: 100% 로깅, sample_rate 메타데이터 없음sample_rate 메타데이터를 추가하여 역확률 가중치 보정 가능// shouldSampleEvent() 반환값에 따른 동작
const sampleResult = shouldSampleEvent(eventName)
if (sampleResult === null) {
// 설정 없음 — 100% 로깅, sample_rate 메타데이터 없음
dispatch(event)
} else if (sampleResult === 0) {
// 드롭 — 이 이벤트 타입 완전 비활성화
return
} else {
// 확률적 샘플링 — 역확률 가중치용 메타데이터 추가
event.metadata.sample_rate = sampleResult
if (Math.random() < sampleResult) {
dispatch(event)
}
}
샘플링 비율이 0.1인 이벤트는 10%만 수집됩니다. 분석 시 각 이벤트에 1 / sample_rate (= 10) 가중치를 곱하면 원래의 전체 이벤트 수를 추정할 수 있습니다. sample_rate 메타데이터가 이벤트와 함께 전송되므로 분석 파이프라인에서 정확한 역보정이 가능합니다.
index.ts(공개 API + 큐), sink.ts(라우터), datadog.ts(전송), metadata.ts(강화), growthbook.ts(플래그), sinkKillswitch.ts(킬스위치)attachAnalyticsSink() 전 이벤트를 버퍼링하고, queueMicrotask로 비동기 드레인합니다_PROTO_ 접두사 키는 1P 전용 BigQuery로 라우팅되고, Datadog 전송 전 stripProtoFields()로 제거됩니다never 타입 마커(AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)로 PII 유출을 컴파일 타임에 신호합니다tengu_frond_boric는 실패 개방 설계 — 설정 없으면 싱크 활성 유지, firstParty 킬 시 디스크 큐잉 후 재개shouldSampleEvent() null은 100% 로깅, 비율 값은 역확률 가중치용 sample_rate 메타데이터를 추가합니다Q1. attachAnalyticsSink() 전에 logEvent()가 호출되면 어떻게 되나요?
eventQueue에 푸시됩니다. attachAnalyticsSink()가 호출되면 queueMicrotask를 사용하여 대기 중인 모든 이벤트를 비동기적으로 드레인합니다. 이벤트는 삭제되지 않습니다.Q2. shouldSampleEvent()에 해당 이벤트의 설정 항목이 없을 시 반환값은?
null을 반환합니다. 이는 100% 로깅을 의미하지만, sample_rate 메타데이터가 추가되지 않습니다. 비율 값이 있는 경우에만 sample_rate 메타데이터가 역확률 가중치 보정용으로 추가됩니다.Q3. AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS가 never 타입인 이유는?
never는 TypeScript의 바텀 타입으로 어떤 값도 할당할 수 없습니다. 값을 할당하려면 반드시 as 캐스트가 필요하고, 이때 개발자와 코드 리뷰어가 "이 값이 코드나 파일 경로가 아님을 확인했다"라는 타입 이름을 읽게 됩니다. 컴파일 타임 페이퍼 트레일입니다.Q4. 1P와 고객용 LoggerProvider의 관계는?
logs.setGlobalLoggerProvider()를 통해 전역으로 등록됩니다. 이 분리를 통해 1P 텔레메트리와 고객 텔레메트리가 독립적으로 작동합니다.Q5. firstParty 킬스위치가 true로 설정되면?