분석 & 텔레메트리

제로 의존성 분석 API, 이중 파이프라인 라우팅, Datadog, 메타데이터 강화, PII 위생, GrowthBook 기능 플래그.

01

개요

Claude Code의 분석 시스템은 제로 의존성 원칙을 따르는 경량 텔레메트리 아키텍처입니다. 외부 분석 SDK 없이 자체 구현된 이벤트 수집, 배치 전송, 메타데이터 강화, PII 위생 처리를 제공합니다. 6개 파일로 구성된 모듈러 아키텍처가 이벤트의 전체 수명 주기를 관리합니다.

다루는 소스 파일: analytics/ 디렉토리 — index.ts(공개 API + 큐), sink.ts(라우터), datadog.ts(Datadog 전송), metadata.ts(메타데이터 강화), growthbook.ts(기능 플래그), sinkKillswitch.ts(긴급 오프 스위치)

6파일 아키텍처의 책임 분리:

02

프리싱크 큐

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인가?

queueMicrotask는 현재 마이크로태스크 큐 끝에 드레인 작업을 스케줄링합니다. setTimeout(0)과 달리 매크로태스크 큐를 사용하지 않으므로 I/O 콜백 이전에 실행됩니다. 이렇게 하면 attachAnalyticsSink() 호출자의 동기 흐름에 지연을 추가하지 않으면서도 대기 중인 이벤트가 빠르게 드레인됩니다.

03

싱크 라우팅

이벤트 메타데이터에서 _PROTO_ 접두사가 붙은 키는 1P(First-Party) 전용 BigQuery 컬럼으로 라우팅됩니다. Datadog으로 전송하기 전에 stripProtoFields()가 이러한 필드를 제거하여 내부 데이터가 외부 서비스로 유출되지 않도록 합니다.

라우팅 흐름

  1. 이벤트 수신 시 _PROTO_ 필드 분리
  2. shouldSampleEvent() 체크 — 샘플링 비율에 따라 이벤트 드롭 또는 통과
  3. 통과한 이벤트를 1P 싱크와 Datadog에 병렬 디스패치
  4. Datadog 전송 전 stripProtoFields()로 내부 전용 필드 제거
주의사항

_PROTO_ 필드 명명 규칙을 어기면 내부 전용 데이터가 Datadog으로 전송될 수 있습니다. 새로운 1P 전용 메타데이터를 추가할 때는 반드시 _PROTO_ 접두사를 사용해야 합니다.

04

Datadog 전송

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% 오차
}
05

메타데이터 강화

모든 이벤트는 전송 전에 세 가지 범주의 메타데이터로 강화됩니다.

EnvContext (환경 컨텍스트)

플랫폼, 아키텍처, 런타임 버전, CI 여부 등 환경 정보를 수집합니다. 이 정보는 세션 시작 시 한 번만 수집하고 메모이즈합니다 — 매 이벤트마다 재수집하면 불필요한 오버헤드가 발생합니다.

ProcessMetrics (프로세스 메트릭)

RSS(Resident Set Size), 힙 사용량, CPU 퍼센트를 수집합니다. EnvContext와 달리 ProcessMetrics는 이벤트별 델타로 기록됩니다 — 시간에 따른 리소스 사용량 변화를 추적합니다.

에이전트 식별

이벤트 발생 컨텍스트에 따라 에이전트를 식별합니다:

딥 다이브 — AsyncLocalStorage로 에이전트 식별

Node.js의 AsyncLocalStorage는 비동기 호출 체인 전체에 걸쳐 컨텍스트를 유지합니다. 서브에이전트가 생성될 때 고유 식별자가 AsyncLocalStorage에 저장되고, 해당 서브에이전트 내의 모든 이벤트에 자동으로 태깅됩니다. 이를 통해 메인 에이전트와 서브에이전트의 이벤트를 정확하게 분리할 수 있습니다.

06

PII 위생

개인 식별 정보(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 캐스트를 하려면 개발자가 "이 값이 코드나 파일 경로가 아님을 확인했다"는 타입 이름을 읽어야 합니다. 코드 리뷰어에게도 해당 캐스트의 의미를 즉시 전달합니다.

런타임 보호

주의사항

never 타입 캐스트는 TypeScript 컴파일러 체크일 뿐 런타임 보호는 아닙니다. 런타임에서의 잘라내기와 일반화 로직이 실제 데이터 유출을 방지합니다. 두 레이어 모두 필요합니다.

07

GrowthBook 기능 플래그

GrowthBook은 기능 플래그와 A/B 테스트를 관리하는 시스템입니다. Claude Code는 원격 평가 모드를 사용합니다.

원격 평가

기능 플래그는 로컬에서 평가되지 않고 GrowthBook 서버에서 평가된 결과를 받아옵니다. 복잡한 타겟팅 규칙을 클라이언트에 배포하지 않아도 됩니다.

사용자 속성

원격 평가에 사용되는 주요 사용자 속성:

3레벨 오버라이드

기능 플래그 값은 세 단계에서 오버라이드할 수 있으며, 우선순위가 높은 순서대로 적용됩니다:

  1. 환경 변수 — 로컬 개발/테스트용 즉시 오버라이드 (최고 우선순위)
  2. 설정 파일 — 사용자 또는 조직 정책에서 오버라이드
  3. 원격 (GrowthBook 서버) — 기본 원격 값 (최저 우선순위)

안전 가드와 노출 중복 제거

08

킬스위치

tengu_frond_boric — 의도적으로 의미 없는 키 이름을 사용하여 우연한 충돌을 방지하는 킬스위치입니다.

독립적 싱크 제어

킬스위치는 Datadog 싱크와 firstParty 싱크를 독립적으로 비활성화할 수 있습니다. 하나의 싱크에 문제가 발생했을 때 다른 싱크에 영향을 주지 않고 개별적으로 끌 수 있습니다.

실패 개방 설계

킬스위치는 실패 개방(fail-open) 설계를 따릅니다:

딥 다이브 — firstParty 킬스위치 동작

firstParty 킬스위치가 true로 설정되면 이벤트가 삭제되지 않습니다. 대신 이벤트가 디스크에 큐잉되고 백오프 타이머가 틱합니다. 킬스위치 플래그가 해제되면 대기 중인 이벤트의 전달이 재개됩니다. 이 설계로 일시적 장애 시에도 이벤트가 유실되지 않습니다.

09

분석 비활성화 조건

킬스위치 외에도 여러 조건에서 분석 시스템이 비활성화됩니다:

예외: 피드백 설문

피드백 설문(사용자 만족도 조사 등)은 서드파티 프로바이더(Bedrock, Vertex 등)에서도 활성 상태를 유지합니다. 프로바이더에 관계없이 제품 경험 데이터를 수집하기 위함입니다.

10

이벤트 샘플링

shouldSampleEvent() 함수가 이벤트별 샘플링을 제어합니다.

샘플링 규칙

// 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 메타데이터가 이벤트와 함께 전송되므로 분석 파이프라인에서 정확한 역보정이 가능합니다.

11

핵심 요약

핵심 포인트

  • 6파일 모듈러 아키텍처: index.ts(공개 API + 큐), sink.ts(라우터), datadog.ts(전송), metadata.ts(강화), growthbook.ts(플래그), sinkKillswitch.ts(킬스위치)
  • 프리싱크 큐가 attachAnalyticsSink() 전 이벤트를 버퍼링하고, queueMicrotask로 비동기 드레인합니다
  • _PROTO_ 접두사 키는 1P 전용 BigQuery로 라우팅되고, Datadog 전송 전 stripProtoFields()로 제거됩니다
  • Datadog 전송은 약 40개 허용 이벤트 큐레이션, 15초/100개 배치, SHA-256 30버킷 해싱(~3.3% 오차)으로 카디널리티를 제한합니다
  • 메타데이터 강화: EnvContext(메모이즈), ProcessMetrics(이벤트별 델타), 에이전트 식별(AsyncLocalStorage)
  • TypeScript never 타입 마커(AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)로 PII 유출을 컴파일 타임에 신호합니다
  • GrowthBook은 원격 평가와 3레벨 오버라이드(환경변수 → 설정 → 원격)를 지원하며, 빈 features 응답 시 디스크 캐시를 보존합니다
  • 킬스위치 tengu_frond_boric는 실패 개방 설계 — 설정 없으면 싱크 활성 유지, firstParty 킬 시 디스크 큐잉 후 재개
  • shouldSampleEvent() null은 100% 로깅, 비율 값은 역확률 가중치용 sample_rate 메타데이터를 추가합니다
12

지식 확인

퀴즈 — 5문제

Q1. attachAnalyticsSink() 전에 logEvent()가 호출되면 어떻게 되나요?

  • A) 이벤트 삭제
  • B) 인메모리 큐에 푸시, 싱크 연결 시 드레인
  • C) 디스크에 저장
  • D) 오류 발생
싱크가 연결되기 전의 이벤트는 eventQueue에 푸시됩니다. attachAnalyticsSink()가 호출되면 queueMicrotask를 사용하여 대기 중인 모든 이벤트를 비동기적으로 드레인합니다. 이벤트는 삭제되지 않습니다.

Q2. shouldSampleEvent()에 해당 이벤트의 설정 항목이 없을 시 반환값은?

  • A) 0 (드롭)
  • B) 1 (100%)
  • C) null (100% 로깅, sample_rate 메타데이터 없음)
  • D) 오류
설정 항목이 없는 이벤트는 null을 반환합니다. 이는 100% 로깅을 의미하지만, sample_rate 메타데이터가 추가되지 않습니다. 비율 값이 있는 경우에만 sample_rate 메타데이터가 역확률 가중치 보정용으로 추가됩니다.

Q3. AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHSnever 타입인 이유는?

  • A) 성능 최적화를 위해
  • B) never는 바텀 타입 — 명시적 as 캐스트를 강제하여 코드 리뷰 신호 역할
  • C) TypeScript 제한 때문
  • D) 호환성을 위해
never는 TypeScript의 바텀 타입으로 어떤 값도 할당할 수 없습니다. 값을 할당하려면 반드시 as 캐스트가 필요하고, 이때 개발자와 코드 리뷰어가 "이 값이 코드나 파일 경로가 아님을 확인했다"라는 타입 이름을 읽게 됩니다. 컴파일 타임 페이퍼 트레일입니다.

Q4. 1P와 고객용 LoggerProvider의 관계는?

  • A) 동일 인스턴스
  • B) 1P는 모듈 로컬, 전역 등록 안 함; 고객 프로바이더는 logs.setGlobalLoggerProvider()
  • C) 고객이 1P 래핑
  • D) 상호 배타
1P LoggerProvider는 모듈 로컬로 유지되며 전역 등록하지 않습니다. 반면 고객용 프로바이더는 logs.setGlobalLoggerProvider()를 통해 전역으로 등록됩니다. 이 분리를 통해 1P 텔레메트리와 고객 텔레메트리가 독립적으로 작동합니다.

Q5. firstParty 킬스위치가 true로 설정되면?

  • A) 이벤트 삭제
  • B) 이벤트 디스크 큐잉, 백오프 타이머 틱, 플래그 해제 시 전달 재개
  • C) 프로세스 종료
  • D) Datadog으로 리라우팅
firstParty 킬스위치가 활성화되면 이벤트가 삭제되지 않습니다. 대신 디스크에 큐잉되고 백오프 타이머가 틱합니다. 킬스위치 플래그가 해제되면 대기 중인 이벤트의 전달이 재개됩니다. 이 설계로 일시적 장애 시에도 이벤트 유실을 방지합니다.
0 / 5