콘텐츠로 이동

Product Analytics & Tracking Plan

분류: Layer 13 - Product Engineering & Growth Systems

Product Engineer는 기능이 배포된 뒤 “잘 됐나요?”라는 질문을 재현 가능한 데이터로 바꿀 수 있어야 한다. 이때 필요한 것은 클릭 이벤트 몇 개가 아니라, 제품 질문 -> 지표 정의 -> 이벤트 의미 -> 코드와 검증을 연결하는 tracking plan이다.

Product Analytics(제품 분석) 는 사용자가 제품에서 무엇을 하고 어디서 이탈하며 어떤 행동을 반복하는지 관찰해 제품 결정을 돕는 활동이다. 매출 보고나 서버 상태 관측만을 뜻하지 않는다. 사용자의 행동이 제품 질문에 답할 수 있는 형태로 기록되어야 한다.

Event(이벤트) 는 제품에서 이미 일어난 하나의 사실을 정해진 이름과 시각, 식별자, 속성으로 표현한 레코드다. invite_created는 “초대 생성이 성공했다”는 사실이고, invite_create_button_clicked는 “버튼을 눌렀다”는 UI 행동이다. 둘은 가까워 보여도 성공 여부와 발생 주체가 다르다.

Property(속성) 는 이벤트를 분석 가능한 축으로 나누는 부가 값이다. workspace_id, source, actor_role, plan이 예다. 이름이 행동을 말한다면 property는 그 행동이 어떤 맥락에서 일어났는지를 말한다.

Event taxonomy(이벤트 분류 체계) 는 제품 행동을 일관된 이름, 시제, 범주, 공통 property로 표현하는 규칙이다. Tracking plan(추적 계획) 은 제품 질문을 지표와 이벤트로 내리고, 각 이벤트의 trigger(발생 조건), identity(식별 기준), schema(데이터 구조), owner(책임자), privacy class(개인정보 등급), validation(검증 방법)까지 적은 데이터 계약이다.

Instrumentation(계측) 은 이 계약에 맞춰 실제 클라이언트, 서버, worker가 이벤트를 만들고 전송하도록 구현하는 일이다. tracking plan이 설계도라면 instrumentation은 실행 코드다. 이벤트가 전송됐다는 사실만으로 tracking plan이 완성되지는 않는다.

2. 선행 방식의 한계 - 왜 Tracking Plan이 등장했는가

섹션 제목: “2. 선행 방식의 한계 - 왜 Tracking Plan이 등장했는가”

운영 로그와 analytics 이벤트를 같은 감각으로 다루면 초반에는 빠르다. “필요해 보이는 곳에 로그를 찍고, 나중에 쿼리하면 된다”는 방식이다. 하지만 운영 로그는 주로 시스템이 왜 실패했는지 조사하는 데 쓰이고, 제품 이벤트는 사용자가 어떤 흐름을 거쳐 어떤 결과에 도달했는지 비교하는 데 쓰인다.

제품 이벤트에는 운영 로그보다 긴 의미 수명이 필요하다. 화면 이름은 바뀌어도 같은 행동의 시계열은 이어져야 하고, 클라이언트와 서버에서 발생한 사건은 같은 사용자와 workspace로 연결되어야 한다. 실험 variant, 개인정보 수집 목적, 중복 재시도까지 통제하지 않으면 이벤트는 많아져도 funnel과 cohort는 서로 다른 사실을 말한다.

예를 들어 초대 버튼 클릭을 100건 기록했더라도 다음을 모르면 초대 생성률을 계산할 수 없다. 여기서 API(Application Programming Interface, 프로그램 사이의 호출 계약)는 버튼 뒤에서 초대 생성을 요청하는 서버 경계다.

  • 한 사람이 재시도한 3번을 3명으로 셌는가?
  • API가 실패한 클릭도 성공으로 셌는가?
  • 같은 workspace의 관리자 두 명을 하나로 볼 것인가?
  • 모바일 재전송과 서버 재시도가 중복 이벤트를 만들었는가?
  • 이벤트 이름이나 required property가 배포 중간에 바뀌었는가?
  • 분석 수집이 허용되지 않은 사용자까지 포함했는가?

tracking plan은 이 문제를 “더 많이 수집”해서 풀지 않는다. 답하려는 질문에 필요한 최소 사실을, 같은 의미로, 한 번씩, 허용된 범위에서 기록한다는 철학으로 푼다. Product analytics 도구가 있어도 이 계약이 없으면 도구는 의미가 다른 레코드를 빠르게 집계할 뿐이다.

3. 제품 질문을 먼저 지표 계약으로 좁힌다

섹션 제목: “3. 제품 질문을 먼저 지표 계약으로 좁힌다”

이벤트 설계는 코드 위치나 화면 요소에서 시작하지 않는다. 먼저 제품 질문과 계산 단위를 작게 만든다.

제품 질문필요한 행동 신호이벤트 후보
사용자가 첫 가치를 경험했는가activation 행동 완료project_created, api_call_succeeded
사용자가 핵심 흐름에서 어디서 이탈하는가단계별 시작·실패·완료invite_modal_opened, invite_created, invite_accepted
새 UX가 더 좋은가실험군별 전환과 guardrailonboarding_completed, error_seen
유료 기능이 실제로 쓰이는가entitlement 기반 기능 사용premium_export_completed

여기서 metric contract(지표 계약) 는 지표 이름만 적은 문서가 아니다. 최소한 cohort(같은 시작 조건으로 묶은 집단), numerator(분자), denominator(분모), window(관찰 기간), analysis unit(집계 단위), deduplication(중복 제거)을 고정해야 한다. invite_accepted rate처럼 분모와 기간이 없는 이름은 서로 다른 세 지표를 한 단어로 섞으므로 사용하지 않는다.

3.1. 초대 흐름에서 구분해야 할 세 지표

섹션 제목: “3.1. 초대 흐름에서 구분해야 할 세 지표”

Discovery 문서에서 정한 공통 흐름은 다음과 같다.

invite_modal_opened -> invite_created -> invite_accepted
invite_modal_opened -> invite_modal_closed

하지만 같은 이벤트를 사용해도 질문에 따라 cohort와 분모가 다르다.

Qualifying modal(지표 포함 모달) 은 초대 권한이 있는 사용자의 화면에서 초대 UI가 실제로 visible·interactive 상태가 되고 workspace_id, flow_id, modal_view_id가 모두 발급된 노출이다. prefetch, background render, 권한 거절, 렌더링 실패는 포함하지 않는다. flow_id는 모달을 연 뒤 닫거나 timeout될 때까지의 한 초대 시도를, modal_view_id는 그 시도 안의 한 실제 모달 노출을 식별하며 open·close 보조 이벤트가 같은 두 값을 공유한다. invite_id는 생성된 Invite 사업 객체를 create와 accept 사이에서 join하고 metric uniqueness를 계산하는 키다. domain_event_id는 서버가 성공 사실의 재전송을 구분하기 위해 한 번 발급하고 계속 유지하는 필수 ID다. transaction/outbox와 수집 단계 중복 제거의 상세 메커니즘은 뒤에서 설명한다.

지표cohort와 window분자 / 분모 / dedup답하는 질문
invite_creation_rate분석 기간의 qualifying modal workspace cohort. invite_modal_opened.flow_id = invite_created.flow_id이고 create 시각은 [opened_at, min(invite_modal_closed.closed_at, opened_at + 30m)) 안이어야 한다. close가 유실되면 opened_at + 30m를 종료 시각으로 사용한다.window 안에 invite_created가 뒤따른 unique workspace / qualifying invite_modal_opened가 있는 unique workspace. workspace당 한 번만 센다.관리자가 초대 UI를 본 뒤 초대 생성까지 도달했는가?
7일 accepted/modal funnel outcome위와 같은 qualifying modal workspace cohort를 각 cohort 진입 시각부터 7일 관찰한다. modal→created는 같은 flow_id와 creation window를, created→accepted는 invite_created.invite_id = invite_accepted.invite_id를 요구한다.연결된 invite 중 하나라도 7일 안에 invite_accepted가 된 unique workspace / qualifying modal-open unique workspace. workspace당 한 번만 센다.초대 UI 진입이 실제 membership 생성까지 이어졌는가?
새 workspace 7일 activation_rate분석 기간에 생성되고 제품 정책상 초대 가능한 eligible new workspace cohort를 workspace_created부터 7일 관찰한다. 모달 노출 여부와 무관하게 모든 eligible workspace를 분모에 남기는 ITT(Intention-to-Treat, 실제 진입 여부와 관계없이 시작 집단 전체를 유지하는 분석) 방식이다.같은 workspace_id 안에서 invite_created.invite_id = invite_accepted.invite_id로 연결해 7일 안에 생성·수락을 모두 마친 unique workspace / eligible new unique workspace. invite_modal_opened, flow_id, modal_view_id를 요구하지 않고 workspace당 한 번만 센다.새 workspace가 협업 가치의 초기 신호에 도달했는가?

첫 번째와 두 번째 지표는 invite_modal_opened가 분모지만, 세 번째는 eligible workspace_created가 분모다. 여기서 eligible은 enrollment 구간 [start_at, end_at)에 production에서 처음 생성되고 creator가 admin이며, 생성 시점에 is_internal = false, is_test = false, is_automated = false인 workspace다. 사전에 선언한 region·plan 조건이 있다면 같은 생성 시점 값으로 적용하고 결과를 본 뒤 제외 조건을 추가하지 않는다. API나 다른 정상 비모달 경로로 생성·수락한 workspace도 activation 분자에 포함한다. 관찰 기간이 둘 다 7일이어도 모집단이 다르므로 accepted/modal funnel outcome을 activation rate로 부르면 안 된다. invite_created를 분모로 두고 7일 내 invite_accepted workspace를 세는 값은 별도의 recipient diagnostic(수신자 구간 진단 지표) 이다. 이는 생성 이후 전달·열람·만료·수락 구간을 진단하며 invite_creation_rate를 대체하지 않는다.

modal-open cohort에서 한 workspace가 여러 번 모달을 열면 분석 기간 안의 첫 qualifying invite_modal_opened 시각을 cohort 진입 시각으로 고정한다. 이후의 모달 노출은 별도 flow_id로 품질을 진단할 수 있지만, 지표 분모와 7일 window를 다시 시작시키지는 않는다.

3.2. 숫자로 보면 왜 이름만으로 부족한가

섹션 제목: “3.2. 숫자로 보면 왜 이름만으로 부족한가”

같은 주에 modal-open workspace가 1,000개이고 그중 720개가 초대를 생성했다고 하자. invite_creation_rate720 / 1,000 = 72%다. 7일 안에 230개 workspace에서 수락이 일어났다면 accepted/modal funnel outcome은 230 / 1,000 = 23%, recipient diagnostic은 230 / 720 ≈ 31.9%다.

같은 주에 eligible new workspace는 400개였고, 7일 안에 생성과 수락을 모두 마친 workspace가 80개라면 신규 workspace 7일 activation_rate80 / 400 = 20%다. 네 값은 같은 이벤트를 일부 공유하지만 제품 질문이 다르다. 대시보드에 단순히 “invite accepted 23%“라고 쓰면 어떤 값인지 재현할 수 없다.

invite_modal_closed는 정상 종료 시 정확한 closed_at을 제공하지만 브라우저 종료, process kill, 네트워크 단절에서는 유실될 수 있다. 그래서 open 뒤 close가 없으면 opened_at + 30m를 fallback 종료로 사용한다. closed_at < opened_at이거나 다른 flow_id에 속한 close는 close row만 데이터 품질 오류로 격리하고, 유효한 open에는 같은 30분 fallback을 적용한다. 오른쪽 경계는 포함하지 않는 반열린 구간 [opened_at, creation_window_end)이다. 30분은 보편 법칙이 아니라 이 worked example의 metric contract다. 실제 사용자가 초대 정보를 검토하는 데 더 긴 시간이 필요하다는 근거가 있다면 window를 바꿀 수 있지만, baseline과 target을 같은 정의로 다시 계산하고 metric version을 남겨야 한다.

또한 7일 window가 끝나지 않은 최근 cohort를 완결된 cohort와 섞으면 아직 수락할 시간이 없었던 workspace가 실패로 계산된다. 이를 right censoring(우측 검열) 이라고 한다. 7일 지표는 관찰 기간이 모두 지난 cohort만 확정값으로 비교하거나, 미성숙 cohort임을 별도로 표시한다.

4. Event Taxonomy는 제품의 행동 언어다

섹션 제목: “4. Event Taxonomy는 제품의 행동 언어다”

좋은 이벤트 이름은 UI(User Interface, 사용자가 제품과 상호작용하는 화면) 구조보다 사용자의 행동을 표현한다. button_clicked보다 invite_created가 낫고, modal_submit보다 workspace_created가 낫다. 화면 이름과 버튼 위치는 sourcesurface property로 남길 수 있지만, 핵심 event는 제품 행동이어야 한다.

Event Taxonomy 설계 규칙

행동 중심 이름

UI 컴포넌트가 아니라 사용자의 제품 행동을 이름으로 둔다.

좋음: invite_created / 약함: submit_button_clicked

일관된 시제

성공적으로 끝난 행동은 과거형, 노출은 viewed/opened처럼 구분한다.

예: checkout_started, checkout_completed, checkout_failed

공통 property

workspace_id, plan, role, source 같은 공통 축과 조건부 experiment_id/variant를 표준화한다.

대시보드와 cohort가 같은 필터를 쓸 수 있다.

민감정보 제외

email, 이름, 원문 입력값처럼 식별 가능하거나 민감한 값은 기본 제외한다.

필요하면 별도 동의·마스킹·접근·보존 정책을 세운다.

이벤트는 사용자 행동의 압축 표현이다. 이름은 행동을, property는 분석 축을, occurred_at은 실제 발생 순서를, identity는 집계 단위를 담당한다. sent_at이나 warehouse 적재 시각만으로 순서를 잡으면 오프라인 재전송과 worker 지연 때문에 funnel 단계가 뒤집힐 수 있다.

taxonomy는 이벤트 수를 많이 만드는 분류표가 아니다. 같은 사업 사실에 여러 이름이 생기는 naming drift(명명 표류) 와, 같은 이름의 의미가 배포마다 달라지는 semantic drift(의미 표류) 를 막는 공통 언어다.

제품 전체에는 같은 원칙을 적용하되 흐름별 행동 언어는 구체적이어야 한다.

제품 흐름핵심 이벤트주의할 property
Onboardingworkspace_created, first_project_createdsource, role, template_id, time_to_value_bucket
Checkoutcheckout_started, checkout_completed, payment_failedplan, billing_cycle, error_code, provider_category
Dashboard adoptiondashboard_saved, dashboard_sharedwidget_count_bucket, viewer_role, source
Deletion/privacy UXdeletion_requested, data_export_startedscope, request_source, retention_policy_key

tracking plan은 문서, 스프레드시트, 코드 schema 중 어떤 형태여도 된다. 중요한 것은 이벤트 이름만 나열하지 않고 다음 필드를 한 행의 계약으로 묶는 것이다.

필드설명초대 예시
Event name표준 행동 이름invite_created
Meaning무엇이 참이 되었는가pending Invite가 저장되고 전달 작업이 등록됨
Trigger정확히 어느 조건 뒤에 한 번 발생하는가서버 트랜잭션 commit 뒤
Producer누가 권위 있게 발생시키는가collaboration server
Identityactor와 분석 대상은 누구인가actor_user_id, workspace_id, invite_id
Dedup key재시도를 같은 사실로 묶는 키required domain_event_id
Properties분석에 필요한 맥락invite_role, source, seat_limit_state
Required/optional누락 허용 여부와 타입workspace_id required string
Schema versionpayload 의미의 버전schema_version: 1
Privacy class개인정보·민감정보 등급과 목적pseudonymous product analytics
Destination허용된 전달 대상warehouse, analytics tool
Owner변경 승인과 장애 대응 책임Growth/Workspace PE
QA rule수집 전후 검증 기준schema validation + warehouse reconciliation

MeaningTrigger를 분리해야 한다. “초대 버튼 클릭 시”라는 trigger로 invite_created를 보내면 API 실패에도 생성 이벤트가 생긴다. 반대로 서버의 성공 응답을 받았다는 사실만으로 전송하면 응답 유실 뒤 서버에는 Invite가 있지만 분석 이벤트는 없을 수 있다. 상태를 바꾸는 이벤트는 권위 있는 서버의 commit 또는 그 commit과 함께 저장한 outbox/domain event에서 파생하는 편이 의미를 보존하기 쉽다. Outbox(아웃박스) 는 상태 변경과 발행할 event를 같은 transaction에 저장한 뒤 별도 publisher가 전달하는 패턴이다.

6. Worked Example: 초대 흐름을 관측 계약으로 만든다

섹션 제목: “6. Worked Example: 초대 흐름을 관측 계약으로 만든다”

시나리오

초대 funnel을 분석하고 싶다

관리자가 초대 모달을 열고, 초대를 생성하며, 수신자가 링크를 수락하면 Membership(User와 Workspace 사이의 활성 관계 엔티티)이 생긴다. 현재는 버튼 클릭 이벤트만 있고 클라이언트 재시도도 발생한다.

세 단계 각각의 발생 조건, 식별자, 중복 제거 단위를 어떻게 고정해야 하는가?

6.1. 먼저 사업 사실과 관측 지점을 고정한다

섹션 제목: “6.1. 먼저 사업 사실과 관측 지점을 고정한다”
Eventtrigger와 produceridentitydedup/unique 단위
invite_modal_openedqualifying modal이 실제로 visible·interactive 상태가 된 순간 client가 발생시킨다. 클릭만 하고 렌더링에 실패한 경우는 제외하며 occurred_atopened_at으로 해석한다.actor_user_id, workspace_id, 한 번의 진입을 나타내는 flow_id, 한 노출의 modal_view_id수집 중복은 (event, modal_view_id)로 제거한다. 지표에서는 cohort 기간의 unique workspace_id를 한 번 센다.
invite_modal_closed같은 모달이 close 버튼, 취소, 바깥 클릭, route 이동, 생성 성공으로 visible 상태를 벗어난 순간 client가 발생시킨다. occurred_atclosed_at으로 해석한다. close event가 유실되면 지표에서 open+30분으로 종료를 보완한다.open과 동일한 actor_user_id, workspace_id, flow_id, modal_view_id; 종료 이유를 close_reason으로 기록한다.수집 중복은 (event, modal_view_id)로 제거한다. lifecycle 보조 이벤트이며 생성·수락 지표의 분자가 아니다.
invite_created서버가 pending Invite를 저장하고 전달 job/outbox를 같은 성공 경계에 등록한 뒤 발생시킨다. 버튼 클릭이나 이메일 전달 완료가 trigger가 아니다.actor_user_id는 inviter, workspace_id, invite_id. UI modal 경로에서만 flow_id, modal_view_id가 conditionally required이고 API 등 정상 비모달 경로에서는 absent다.transaction/outbox에서 한 번 생성한 required domain_event_id로 재시도를 제거한다. invite_id는 join과 metric uniqueness에 사용하며 ingestion dedup fallback이 아니다. 생성 지표에서는 unique workspace_id를 센다.
invite_accepted서버 트랜잭션이 Invite를 pending -> accepted로 바꾸고 Membership을 만든 뒤 발생시킨다. 링크 열기나 로그인 완료가 trigger가 아니다.actor_user_id는 수락 사용자, inviter_user_id, workspace_id, invite_id, membership_id도메인 InviteAccepted 하나를 analytics event 하나로 매핑하고 transaction/outbox에서 한 번 생성한 required domain_event_id로 재시도를 제거한다. invite_id는 create와 join하고 지표에서는 unique workspace_id를 센다.

이 계약은 Domain 문서의 경계와 같다. invite_accepted는 성공한 수락과 Membership 생성을 세며, 관리자 직접 추가나 SCIM(System for Cross-domain Identity Management, 조직 계정 자동 프로비저닝)으로 생긴 Membership은 포함하지 않는다. 별도의 workspace_member_activated를 운영한다면 Invite 없이도 발생할 수 있으므로 두 이벤트를 같은 지표 분자로 섞지 않는다.

이 문서의 canonical server event에서 domain_event_id는 선택값이 아니다. 상태 transaction과 outbox row를 만들 때 한 번 생성하고, publisher·collector 재시도에도 같은 값을 유지한다. 이행 중인 legacy producer가 이 값을 보내지 못하면 해당 event를 quarantine dataset으로 분리해 누락량과 producer version을 진단한다. invite_id(event, invite_id) 같은 추정 key로 ingestion dedup을 보완해 canonical event나 canonical metric에 합치지 않는다. 생산 계약을 고친 뒤 안정적인 domain_event_id가 있는 event만 위 지표에 포함한다.

Domain 문서와의 이름 대응도 고정한다. 도메인 InviteAccepted.event_id는 analytics의 domain_event_id로 그대로 전달하고, analytics envelope의 별도 event_id는 collector가 다루는 전달 레코드 ID다. 두 ID를 바꾸어 쓰면 도메인 상태 전이 재시도와 수집 레코드 재전송을 구분할 수 없다.

성공 흐름 바깥의 invite_create_failed도 원인 진단을 위해 남긴다. 서버가 초대 생성 요청을 validation, 권한, seat limit 같은 이유로 거절한 시점에 발생시키고, request_id, actor_user_id, workspace_id, error_code, seat_limit_state를 기록한다. UI modal 경로에서만 flow_id, modal_view_id를 함께 보낸다. 같은 요청의 재전송은 (event, request_id)로 제거한다. 이 diagnostic event는 실패 이유를 설명하지만 canonical success event나 invite_created·invite_accepted 지표의 대체 분자가 아니다.

초대 흐름을 연결하는 flow_id는 modal-open과 modal 경로 create의 한 시도를 묶는다. 수락은 다른 사용자·기기·시간에 일어나므로 flow_id만 믿지 않고 invite_id로 create와 accepted를 연결한다. API처럼 modal을 거치지 않는 정상 생성에는 flow_idmodal_view_id가 없어도 된다. 즉, actor identity와 surface가 단계마다 달라도 제품 대상인 workspace_id와 사업 객체인 invite_id가 create 이후 흐름을 이어 준다.

invite_modal_opened
required: event_id, occurred_at, schema_version, sampling_policy_version,
actor_user_id, workspace_id, flow_id, modal_view_id,
actor_role, source, plan
invite_modal_closed
required: event_id, occurred_at, schema_version, sampling_policy_version,
actor_user_id, workspace_id, flow_id, modal_view_id,
close_reason
invite_created
required: event_id, domain_event_id, occurred_at, schema_version,
sampling_policy_version,
actor_user_id, workspace_id, invite_id,
actor_role, invite_role, source, surface, seat_limit_state
conditionally required when surface=invite_modal: flow_id, modal_view_id
absent when surface=api or another non-modal path: flow_id, modal_view_id
invite_accepted
required: event_id, domain_event_id, occurred_at, schema_version,
sampling_policy_version,
actor_user_id, inviter_user_id, workspace_id,
invite_id, membership_id, invite_role, time_to_accept_bucket

experiment_idvariant는 후속 실험 문서와 공유하는 experiment envelope다. 해당 사용자가 실험에 배정되고 이 event가 그 노출 맥락에 속할 때만 두 property를 함께 conditionally required로 보내며, 실험과 무관한 event에는 둘 다 보내지 않는다. 한쪽만 있으면 어느 실험의 어느 variant인지 재현할 수 없으므로 schema validation에서 거절한다.

email 원문, 이름, 초대 메시지 원문은 넣지 않는다. 분석 질문은 “누구의 이메일인가”가 아니라 “어떤 workspace·role·source·seat 상태에서 생성과 수락이 일어났는가”이기 때문이다. invite_id는 무작위 내부 식별자여도 개인과 연결될 수 있는 pseudonymous identifier(가명 식별자) 이므로 공개 데이터로 취급하지 않고 접근·보존 정책을 적용한다.

6.3. 중복은 전송 횟수가 아니라 사업 사실 기준으로 제거한다

섹션 제목: “6.3. 중복은 전송 횟수가 아니라 사업 사실 기준으로 제거한다”

네트워크 timeout 뒤 클라이언트가 같은 생성 요청을 재시도하고, 서버 event publisher도 한 번 재전송했다고 하자. collector에는 invite_created가 4건 도착할 수 있다. 상태 transaction/outbox가 invite_id = inv-42를 만든 사업 사실에 domain_event_id = evt-42를 한 번 발급하고 모든 재시도에서 유지하면 수집 단계에서 하나로 줄일 수 있다. event_id가 전송 시도마다 달라져도 canonical ingestion dedup은 불변인 domain_event_id를 사용한다.

여기서 두 중복 제거를 구분한다.

  1. Ingestion deduplication(수집 중복 제거) 은 같은 canonical server event의 재전송을 필수 domain_event_id로 한 건으로 만든다. 누락 event는 quarantine하며 invite_id로 대체하지 않는다.
  2. Metric uniqueness(지표 고유성) 는 유효한 서로 다른 이벤트가 여러 건이어도 질문에 맞춰 unique workspace, user, invite 중 하나만 센다.

한 workspace가 실제로 초대 5개를 만들었다면 ingestion에서는 5건이 모두 유효하다. 하지만 invite_creation_rate에서는 workspace 하나로 센다. 반대로 초대 처리량을 묻는다면 unique invite_id 5개를 세야 한다. 무조건 COUNT(DISTINCT workspace_id)를 쓰는 것도, 무조건 event row 수를 세는 것도 정답이 아니다.

7. Identity는 “누가”와 “무엇을” 분리한다

섹션 제목: “7. Identity는 “누가”와 “무엇을” 분리한다”

Identity resolution(식별자 해소) 은 anonymous device, 로그인 user, account/workspace처럼 여러 식별자를 같은 분석 주체에 연결하는 규칙이다. 이 규칙이 없으면 로그인 전후가 다른 사람처럼 갈라지고, 과도하게 합치면 공용 기기의 서로 다른 사용자가 한 사람처럼 보인다.

초대 흐름은 인증된 관리자 기능이므로 actor_user_id를 required로 둘 수 있다. 그래도 지표의 analysis unit은 질문에 따라 달라진다.

  • 사용자가 모달을 이해하고 제출했는가: unique actor_user_id
  • workspace가 초대 기능을 채택했는가: unique workspace_id
  • 초대가 전달 후 수락됐는가: unique invite_id
  • Membership이 실제로 몇 개 생겼는가: unique membership_id

actor와 subject도 구분한다. invite_created의 actor는 inviter지만 대상은 아직 계정이 없을 수 있다. invite_accepted의 actor는 수락 사용자이고 inviter는 별도 property다. email을 identity 대신 쓰면 주소 변경, 대소문자·alias 정규화, 개인정보 삭제에서 계약이 흔들린다.

identity merge 정책이 바뀌면 과거 cohort도 재계산될 수 있다. 따라서 dashboard에는 user/workspace merge 규칙과 적용 시점을 남기고, 실험 중간에 identity 정책을 바꿨다면 전후 수치를 같은 시계열로 단순 비교하지 않는다.

8. Schema와 Versioning은 의미 변화를 통제한다

섹션 제목: “8. Schema와 Versioning은 의미 변화를 통제한다”

Schema(스키마) 는 이벤트 property의 이름, 타입, 필수 여부, 허용 값, 중첩 구조를 기계가 검사할 수 있게 표현한 계약이다. Schema evolution(스키마 진화) 은 제품 변경에 따라 이 계약을 바꾸면서 기존 producer, consumer, 저장 데이터와의 호환성을 관리하는 일이다.

이 레이어의 분석·실험·RUM 이벤트는 최소 공통 envelope로 event_id, occurred_at, schema_version, sampling_policy_version을 가진다. sampling_policy_version은 표본 이벤트에만 붙는 선택값이 아니다. 전량 수집도 full-v1, page view 20% 표본은 rum-pageview-20-v1처럼 명시하여 어떤 모집단·key·rate 정책으로 관측했는지 복원한다. 정책이 바뀌면 같은 필드 아래 조용히 섞지 않고 새 version과 적용 시각을 남긴다.

가능하면 tracking plan을 타입이나 JSON(JavaScript Object Notation, 구조화 데이터를 표현하는 텍스트 형식) Schema 같은 실행 가능한 계약과 연결한다. 기존 초대 예시를 공통 envelope까지 포함하면 다음과 같다.

type ExperimentEnvelope =
| { experiment_id: string; variant: string }
| { experiment_id?: never; variant?: never };
type InviteCreatedEventV1 = {
event: "invite_created";
schema_version: 1;
event_id: string;
sampling_policy_version: "full-v1";
domain_event_id: string;
occurred_at: string;
actor_user_id: string;
workspace_id: string;
invite_id: string;
actor_role: "owner" | "admin" | "member";
invite_role: "admin" | "member";
source: "onboarding" | "settings" | "share_modal" | "api";
seat_limit_state: "available" | "near_limit" | "blocked";
} & (
| {
surface: "invite_modal";
flow_id: string;
modal_view_id: string;
}
| {
surface: "api" | "other_non_modal";
flow_id?: never;
modal_view_id?: never;
}
) &
ExperimentEnvelope;

첫 번째 union은 UI modal 경로에서만 flow_idmodal_view_id를 강제하고 API 등 비모달 경로에는 두 값을 허용하지 않는다. ExperimentEnvelopeexperiment_idvariant가 둘 다 있거나 둘 다 없도록 강제한다. 이 타입은 analytics SDK(Software Development Kit, 특정 기능을 코드에서 쓰기 위한 도구 모음) wrapper, backend event publisher, test fixture에서 재사용할 수 있다. 하지만 compile-time type만으로 다른 언어의 producer나 수집 후 데이터까지 보장할 수는 없다. producer의 타입 검사, collector의 runtime validation, data warehouse(분석용 데이터를 통합 보관하는 저장소)의 품질 query가 같은 schema를 바라봐야 한다.

변경은 호환성에 따라 다르게 다룬다.

변경처리 기준이유
optional property 추가같은 version에서 허용 가능하되 consumer의 unknown-field 정책 확인기존 producer와 row가 없어도 의미가 유지된다.
required property 추가이행 기간과 default/backfill을 두거나 새 version 사용구버전 이벤트가 즉시 invalid가 될 수 있다.
enum 값 추가consumer가 unknown 값을 안전하게 처리하는지 확인닫힌 switch가 새 값을 누락할 수 있다.
타입·단위·trigger 의미 변경새 schema version과 metric 전환일 기록같은 필드 이름 아래 의미가 달라지면 시계열이 섞인다.
이벤트 이름 변경같은 사건이면 alias/dual-read 기간, 다른 사건이면 새 event와 새 metricUI 리네임 때문에 행동 시계열을 끊지 않는다.

예를 들어 time_to_accept를 초에서 밀리초로 바꾸면서 이름을 유지하면 배포일 이후 값이 1,000배 커진다. 타입은 여전히 number라 validator가 통과할 수도 있다. 단위는 schema description과 property 이름(time_to_accept_seconds)에 드러내고, 의미 변경은 version과 전환일로 구분해야 한다.

퀴즈

event 이름과 property 타입이 맞는데도 schema version이 필요한 경우는 언제인가?

힌트: 형식이 같아도 의미가 달라질 수 있다.

정답 보기

trigger, 단위, identity, enum 의미처럼 기존 지표 해석을 바꾸는 변경이 있을 때다. 같은 이름으로 섞지 말고 새 version과 metric 전환일을 기록해야 한다.

9. 발생 위치는 권위와 관찰 목적에 따라 고른다

섹션 제목: “9. 발생 위치는 권위와 관찰 목적에 따라 고른다”

Product Engineer는 이벤트를 어디서 발생시킬지도 결정해야 한다.

위치적합한 이벤트주의점
Client노출, 클릭, 입력 시작, abandon 등 UI 행동ad blocker, 네트워크 실패, 중복 발송 가능성
Server결제 성공, 초대 생성, 권한 변경, API 성공UI 맥락 property를 놓치기 쉬움
Worker/Webhook비동기 완료, 결제 갱신, 이메일 발송 결과지연 시간과 멱등성(idempotency) 필요

비즈니스 상태가 바뀌는 이벤트는 그 상태를 commit한 서버나 worker가 더 권위 있다. 사용자가 실제로 무엇을 봤고 어디에서 중단했는지는 클라이언트만 알 수 있다. 그래서 초대 funnel은 client의 invite_modal_opened와 server의 invite_created, invite_accepted를 함께 쓴다.

이 둘을 연결하려고 클라이언트 성공 응답과 서버 성공 이벤트를 모두 invite_created로 보내면 conversion이 부풀려진다. 하나를 canonical producer로 정하고, 다른 신호는 디버깅용 이름으로 분리하거나 보내지 않는다. 서로 다른 위치의 event에는 workspace_id, flow_id, invite_id, occurred_at을 목적에 맞게 이어 주되, 존재하지 않는 시점의 ID를 억지로 만들지 않는다.

10. Consent와 Privacy는 수집 이후 필터가 아니다

섹션 제목: “10. Consent와 Privacy는 수집 이후 필터가 아니다”

Consent(동의) 는 특정 목적의 데이터 처리를 사용자가 이해하고 허용하는 근거다. 모든 제품 분석이 언제나 동의 하나만을 법적 근거로 삼는다는 뜻은 아니다. 지역, 데이터 종류, 목적에 따라 적용 근거와 요구가 달라지므로 제품 정책과 법무 검토가 정한 purpose(목적)·lawful basis(처리 근거)·retention(보존 기간)을 tracking plan에 연결해야 한다.

중요한 경계는 제품의 사업 사실과 analytics 수집을 분리하는 것이다. 초대 수락 트랜잭션과 보안 audit log는 제품 동작과 책임 추적에 필요할 수 있지만, 제3자 analytics destination으로 같은 payload를 보내는 것은 별도의 목적과 정책을 따른다. 사용자가 analytics 수집 대상이 아니어도 초대 기능 자체가 실패해서는 안 된다.

수집 전에는 다음 순서로 판단한다.

  1. 이 property가 어떤 제품 질문에 필요한지 purpose를 적는다.
  2. 집계된 category나 내부 ID로 답할 수 있다면 원문을 수집하지 않는다.
  3. producer에서 consent/policy 상태를 확인하고 허용된 destination만 선택한다.
  4. destination별 접근 권한, 보존 기간, 삭제 전파를 정한다.
  5. consent가 철회되거나 identity가 삭제될 때 과거 데이터 처리 규칙을 검증한다.

email을 hash하면 자동으로 anonymous가 되는 것은 아니다. 사전 대입이 가능하고 다른 데이터와 연결될 수 있으므로 여전히 가명 데이터일 수 있다. 초대 분석에는 email hash 대신 무작위 invite_id, role, source 같은 최소 property가 더 적합하다.

11. Quality와 Validation이 조용한 왜곡을 막는다

섹션 제목: “11. Quality와 Validation이 조용한 왜곡을 막는다”

Data quality(데이터 품질) 는 이벤트가 단순히 존재하는지가 아니라, 계약한 의미·완전성·고유성·적시성·연결성을 유지하는 정도다. tracking 오류는 기능 오류처럼 즉시 화면을 깨뜨리지 않는다. 대시보드가 그럴듯한 숫자를 계속 보여 주므로 발견이 더 늦을 수 있다.

검증은 한 단계의 staging 클릭 테스트가 아니라 생산부터 지표까지 이어지는 층으로 설계한다.

검증 층확인할 계약대표 실패 신호
Producer이름, required property, 타입, trigger새 배포 뒤 특정 version 이벤트가 0건
Collectorschema validation, consent routing, dedup keyinvalid event·duplicate 비율 급증
Warehousenull, enum 분포, event 순서, identity joinworkspace_id null 증가, accepted가 created보다 앞섬
Metriccohort, window, unique unit, mature cohort같은 지표의 dashboard와 SQL 결과 불일치
Release배포 전후 volume과 비율 변화트래픽은 같은데 event volume이 갑자기 2배

11.1. 작은 수치로 품질을 진단한다

섹션 제목: “11.1. 작은 수치로 품질을 진단한다”

배포 전 하루 평균 modal-open unique workspace가 1,000개, raw modal event가 1,080건이었다고 하자. 배포 뒤 unique workspace는 1,020개로 비슷한데 raw event가 2,150건이 되었다면 제품 사용이 두 배가 된 것이 아니다. modal mount 때마다 발행하거나 listener가 중복 등록된 가능성을 먼저 의심해야 한다.

invite_created raw event 720건 중 70건에서 workspace_id가 null이라면, raw row 수를 분자로 쓴 dashboard는 72%를 보여도 workspace에 연결 가능한 생성은 최대 650개이므로 계약한 비율은 최대 65%다. required property 오류는 단순 누락 건수가 아니라 특정 client version, source, plan이 cohort에서 체계적으로 빠지는 selection bias(선택 편향) 를 만들 수 있다.

QA(Quality Assurance, 품질 보증)는 이벤트 한 건을 눈으로 보는 데서 끝나지 않고 계약이 producer부터 metric까지 유지되는지 확인하는 활동이다.

  1. staging에서 핵심 흐름을 직접 수행하고 event inspector로 이름, trigger, property를 확인한다.
  2. 정상·실패·재시도 시나리오로 required 누락, 타입 오류, enum 오타, 중복 발생을 확인한다.
  3. client와 server 이벤트의 occurred_at, identity, correlation key가 이어지는지 본다.
  4. 허용되지 않은 PII(Personally Identifiable Information, 개인 식별 정보)와 destination 전송이 없는지 privacy review를 한다.
  5. warehouse sample query로 cohort, window, dedup을 명시해 세 지표를 재계산한다.
  6. 배포 뒤 event volume, invalid·duplicate·null 비율과 dashboard/SQL(Structured Query Language, 관계형 데이터를 조회하는 언어) 차이를 관찰한다.

퀴즈

tracking plan 없이 이벤트를 나중에 붙이면 가장 먼저 깨지는 것은 무엇인가?

힌트: 이벤트가 있는 것과 분석 가능한 이벤트가 있는 것은 다르다.

정답 보기

이벤트 이름, property, trigger, identity, dedup 단위가 제각각이 되어 funnel, cohort, 실험 분석을 재현하기 어렵다. 특히 required property 누락과 중복 발생은 지표를 조용히 왜곡한다.

반례 1: UI event를 사업 성공으로 사용한다

섹션 제목: “반례 1: UI event를 사업 성공으로 사용한다”

submit_button_clickedinvite_created로 간주하면 validation 실패, seat limit 차단, network timeout도 생성 성공으로 센다. UI 반응을 알고 싶으면 클릭 event를 별도로 두고, 사업 상태 변화는 서버 성공 event로 판단한다.

반례 2: invite_accepted rate 하나로 합친다

섹션 제목: “반례 2: invite_accepted rate 하나로 합친다”

분모를 modal-open, created invite, 신규 workspace 중 무엇으로 두느냐에 따라 23%, 31.9%, 20%가 모두 나올 수 있다. 이름에 cohort와 window를 드러내고 tracking plan에 완전한 식을 남긴다.

반례 3: at-least-once 전송을 사용자 행동 증가로 읽는다

섹션 제목: “반례 3: at-least-once 전송을 사용자 행동 증가로 읽는다”

event pipeline은 유실을 줄이기 위해 같은 message를 재전송할 수 있다. event_id만 매번 새로 만들고 domain key를 버리면 재시도를 제거할 수 없다. transport delivery count와 unique business fact를 분리한다.

반례 4: 필드 이름을 유지하면 호환된다고 본다

섹션 제목: “반례 4: 필드 이름을 유지하면 호환된다고 본다”

invite_role = member가 과거에는 제안 role, 이후에는 수락 시 실제 적용 role을 뜻한다면 string 타입은 같아도 의미가 다르다. 제안은 requested_role, 수락 결과는 applied_role로 분리하거나 schema version을 올린다.

반례 5: 많이 수집한 뒤 나중에 privacy를 처리한다

섹션 제목: “반례 5: 많이 수집한 뒤 나중에 privacy를 처리한다”

원문 email과 초대 메시지를 warehouse에 먼저 적재한 뒤 dashboard에서 숨겨도 최소 수집이 아니다. producer와 routing 단계에서 purpose에 불필요한 property를 차단해야 삭제·접근·유출 범위도 줄어든다.

tracking plan의 깊이는 이벤트의 의사결정 영향과 변경 위험에 맞춘다.

상황최소 선택강화할 신호
일회성 내부 탐색, 의사결정 영향이 작음짧은 event spec과 만료 시점반복 dashboard나 실험 지표로 승격됨
핵심 funnel·activation·결제 행동schema, owner, dedup, metric SQL, release QA여러 producer·destination이 소비함
개인정보·민감 행동purpose, policy gate, 최소 수집, 보존·삭제 검증지역별 정책이나 제3자 destination이 다름
도메인 상태 변경server/domain event를 canonical source로 사용재시도·비동기 처리·여러 consumer가 있음

다음 현상은 tracking plan의 의미 계약이 깨졌다는 신호다.

  • 같은 지표 이름인데 팀별 SQL의 분모나 window가 다르다.
  • UI 배포일을 경계로 conversion이 급변했지만 도메인 성공 건수는 변하지 않았다.
  • unique workspace는 비슷한데 raw event volume만 2배가 되었다.
  • unknown, null, 새 enum 값이 특정 app version에서만 급증한다.
  • invite_accepted가 연결할 invite_created 없이 나타나거나 발생 시각이 반복적으로 앞선다.
  • invite_modal_closed 유실률이 급증해 대부분의 creation window가 open+30분 fallback에 의존한다.
  • consent 철회 뒤에도 차단 대상 destination으로 이벤트가 계속 전달된다.
  • schema version별 비율을 설명할 owner와 전환일이 없다.

이때 먼저 대시보드 문구를 고치기보다 producer trigger, identity join, dedup key, schema version, policy routing을 확인한다. 숫자를 보기 좋게 보정하면 계약 파손이 숨는다.

Tracking Plan 작성 체크

  • 제품 질문이 먼저 있고 metric의 cohort, 분자, 분모, window, unique 단위가 고정되어 있다
  • 이벤트 이름이 UI 요소가 아니라 사용자 행동을 표현한다
  • 각 이벤트의 trigger, canonical producer, identity, dedup key가 명시되어 있다
  • required property와 optional property, 타입, enum, 단위가 구분되어 있다
  • user, actor, invite, workspace 같은 identity 역할과 집계 단위가 일관된다
  • 공통 envelope에 event_id, occurred_at, schema_version, sampling_policy_version이 있고 전량 수집도 full-v1처럼 명시한다
  • schema version과 호환·전환 정책이 있다
  • client/server/worker 이벤트의 역할이 나뉘어 있다
  • 중복, 누락, 순서, volume을 수집 후 검증할 방법이 있다
  • 실험 맥락에는 experiment_id와 variant가 함께 있고, rollout, source, plan 같은 필요한 분석 축만 property에 포함되어 있다
  • PII, consent, purpose, destination, retention 경계가 producer부터 적용된다

tracking plan은 측정의 입력이다. 다음 단계에서는 이 입력으로 funnel, cohort, retention을 해석한다.

  • Funnel, Cohort & Retention Metrics: 이벤트를 제품 흐름 분석으로 바꾼다.
  • Experimentation & Feature Flags: 이벤트를 실험 지표와 rollout 판단에 연결한다.
  • Product Domain Modeling: 분석 event를 실제 사업 상태·도메인 event와 정렬한다.
  • Privacy, Consent & Product Data Governance: 이벤트 수집의 개인정보 경계를 다룬다.
  • Product analytics
  • Event taxonomy
  • Tracking plan
  • Metric contract
  • Required property
  • Schema evolution
  • Identity resolution
  • Ingestion deduplication
  • Analytics QA
  • Client-side analytics
  • Server-side analytics
  • Correlation id
  • Data contract