# 스키마 레퍼런스

이벤트 스키마(`allob.schema.json`)에 쓸 수 있는 모든 필드예요. 스키마는 보통 에이전트가 작성하니, 이 페이지는 에이전트가 참고하거나 사람이 결과를 확인할 때 써요.

## 수정 순서

```sh
allob schema pull                      # 현재 버전을 allob.schema.json 으로 받기
# allob.schema.json 수정
allob schema push --dry-run            # 검사만
allob schema push --note "add checkout_start"
allob sdk pull                         # 새 버전으로 SDK 다시 받기
allob sdk pull --check                 # 배포 전에 — SDK 가 스키마와 같은 버전인지(어긋나면 끝 코드 1)
```

- 올릴 때마다 버전이 하나씩 올라가요. 이전 버전은 `allob schema history` 로 봐요.
- 받은 버전을 기준으로만 올릴 수 있어요. 그 사이 다른 사람이나 에이전트가 먼저 올렸다면 `conflict` 에러가 나요. 다시 `allob schema pull` 해서 합친 뒤 올려요.
- 지금 버전이 받는 이벤트나 저장하는 속성을 지우면 올리지 않고 `breaking_change` 에러와 지워지는 목록을 보여 줘요. 이름을 바꾸거나, 이벤트를 앱과 서버 사이로 옮기거나, `surface` 를 바꾸는 것도 지우는 것으로 봐요. 이미 설치된 앱은 예전 방식으로 계속 보내는데, allob 은 그걸 버리기 때문이에요. 정말 바꾸려면 `allob schema push --allow-breaking` 으로 올려요. 속성의 `column` 만 바꾸면 올리되 경고를 보여 줘요(그 속성의 예전 데이터와 보드에서 이어지지 않아요).
- 올린 스키마는 1분 안에 이벤트 수집에 반영돼요.
- `allob sdk pull --check` 는 아무것도 쓰지 않고 로그인 없이 돌아요. 받은 SDK 의 버전이 `allob.schema.json` 과 다르거나, `allob.schema.json` 을 고치고 아직 올리지 않았거나, SDK 폴더의 파일을 손으로 고쳤거나 지웠으면 이유를 한 줄씩 보여 주고 끝 코드 1 로 멈춰요. 배포 스크립트나 CI 에 넣어요.
  - 줄 끝만 바뀐 것(윈도의 CRLF)은 고친 것으로 보지 않아요. 코드 정리 도구(Prettier 같은 포매터)가 SDK 폴더를 고치면 고친 것으로 보니, SDK 폴더는 포매터 대상에서 빼 두세요.
  - 스키마가 아직 만들 수 없는 대상만 있는 폴더(예: `apps.measurement` 가 없는데 `native` 폴더를 적어 둔 것)는 실패로 치지 않아요. `allob sdk pull` 도 그 대상은 건너뛰기 때문이에요.

## 예시

일기 앱에서 "일기 쓰기 → 결제 화면 → 결제 시작 → 결제"를 측정하는 스키마예요.

```json
{
  "schemaVersion": 1,
  "tracking": {
    "landEvent": "ad_land",
    "allowedHosts": ["diary.example.com"],
    "ga4": { "measurementId": "G-XXXXXXXXXX" }
  },
  "enums": { "plans": ["monthly", "yearly"] },
  "events": {
    "entry_write": {
      "label": "일기 쓰기",
      "props": { "mood": { "enum": ["good", "soso", "bad"] } },
      "order": ["mood"]
    },
    "paywall_view": {
      "label": "결제 화면",
      "props": { "from": { "enum": ["limit", "settings"] } },
      "order": ["from"]
    },
    "checkout_start": {
      "label": "결제 시작",
      "pixel": "InitiateCheckout",
      "ga": "begin_checkout",
      "send": "now",
      "props": { "plan": { "enum": "$plans" }, "price": { "column": "double1" } },
      "order": ["plan"]
    },
    "purchase": {
      "label": "결제(서버)",
      "source": "server",
      "props": { "plan": { "enum": "$plans" }, "value": { "column": "double1" } },
      "order": ["plan"]
    }
  },
  "funnels": {
    "main": { "label": "쓰기 → 결제", "steps": ["entry_write", "paywall_view", "checkout_start", "purchase"] }
  }
}
```

이 스키마로 SDK 를 받으면 `track.entryWrite({ mood: "good" })`, `track.checkoutStart({ plan: "yearly", price: 39 })` 같은 함수가 생겨요. 목록에 없는 값을 넣으면 타입 검사에서 에러가 나요.

## 최상위 필드

| 필드 | 설명 |
|---|---|
| `schemaVersion` | 파일 형식 버전. 지금은 `1` |
| `events` | 이벤트 이름 → 이벤트 정의. 이름은 영문 소문자·숫자·`_`, 40자 이내 |
| `enums` | 여러 이벤트가 같이 쓰는 값 목록. 속성에서 `"$이름"` 으로 불러요 |
| `funnels` | 퍼널 ID → `{ label, steps, board }`. 지표 탭은 `"board": true` 인 퍼널(하나만 둘 수 있어요)을, 없으면 첫 번째 퍼널을 보여 줘요 |
| `eventGroups` | 묶음 이름 → 한 줄 설명. 이벤트의 `group` 필드로 묶어요 |
| `dimensions` | 숫자를 나눠 볼 기준(요금제, 유입 캠페인 등) |
| `experiments` | 제품 안 A/B 실험: 실험 이름 → `{ kind: "assignment", label, arms: [{ id, label, weight }], startedAt, endedAt }`. 그룹(arm)은 기기마다 고정돼요. 아래 [제품 안 A/B](#제품-안-ab) 참고 |
| `tracking` | 측정 설정. 아래 [tracking 필드](#tracking-필드) 참고 |
| `apps` | 앱 측정 모듈 설정 `apps.measurement`. 아래 [apps 필드](#apps-필드) 참고 |

`dimensions`·`experiments` 는 저장하고 검사하지만, 지표 탭에는 아직 퍼널·방문 기기·리텐션·유입 경로·이벤트만 나와요.

## 제품 안 A/B

`experiments` 에 실험을 적고 SDK 를 다시 받으면, 실험마다 그룹을 돌려주는 함수가 생겨요.

```ts
const analytics = build();
const copy = analytics.experiment.pricingCopy(); // "control" | "short"
```

- 그룹은 기기 id 로 정해져서 같은 기기는 늘 같은 그룹이에요. 서버에 묻지 않아요. 웹·앱·서버(`server.ts` 의 `assignArm(실험, sid)`)가 같은 기기 id 면 같은 그룹을 줘요.
- 로그인한 사용자는 `setUserId(사용자 id)` 를 부르면 기기 대신 사용자 id 로 그룹을 정해요. 그래서 같은 사람은 웹·앱·다른 기기에서도 같은 그룹이에요. 사용자 id 는 이벤트에 싣지 않고 광고·분석 도구로도 보내지 않아요. [피드백](/docs/feedback)을 보낼 때만 보낸 사람으로 같이 가요. 로그인을 안 뒤, 실험 함수를 부르기 전에 불러 주세요(이 페이지·이번 실행에서 이미 정한 실험의 그룹은 바뀌지 않아요). 로그아웃하면 `setUserId(null)` 이에요. 서버는 `assignArm(실험, sid, 사용자 id)` 로 같은 그룹을 받아요.
- 처음 부를 때 한 번 노출 이벤트 `experiment_view`(실험·그룹)를 보내요. allob 에만 보내고 광고·분석 도구로는 보내지 않아요. 앱은 `start()` 뒤에 불러 주세요.
- 그룹의 `label` 은 지표 화면에 보이는 이름이에요(없으면 `id`). `weight` 는 나누는 비율이에요(없으면 1). `startedAt`·`endedAt` 은 실험을 시작하고 끝낸 날이에요. 지표 탭의 변경 타임라인에 그날 점으로 보이고, 결과도 이 기간으로 잘라 봐요.
- 계측이 꺼진 기기(추적 거부·동의 전)도 그룹은 받지만 노출은 보내지 않아요.
- `setUserId` 없이 로그인 등으로 기기 id 를 바꾸면(`adoptSid`) 그 뒤로 그룹이 바뀔 수 있어요. 지표에서는 그런 기기를 처음 그룹으로만 세고, 몇 대인지 알려 줘요.
- 이벤트 속성에 그룹을 싣던 방식(`dimension`)도 그대로 쓸 수 있어요.
- 결과는 지표 탭의 퍼널 나눠 보기에서 "실험: 이름"을 고르거나 `allob metrics funnel --by exp:<실험>` 으로 봐요([지표 — 제품 안 A/B 결과](/docs/metrics#제품-안-ab-결과)).

## 이벤트 필드

| 필드 | 설명 |
|---|---|
| `label` | 화면에 보이는 이름 |
| `props` | 속성 이름 → 속성 정의 |
| `order` | 값 목록 속성(`enum`)의 순서. 값 목록 속성이 있으면 꼭 적어야 해요 |
| `source` | `"server"` 면 서버 이벤트. 웹·앱 SDK 에는 생기지 않고 `server.ts` 에 생겨요 |
| `surface` | `"web"` 또는 `"app"`. 기본값은 `tracking.surface`(없으면 `web`) |
| `send` | `"now"` 면 모으지 않고 바로 보내요. 결제 바로 앞 단계처럼 놓치면 안 되는 이벤트에 써요 |
| `pixel`·`ga`·`tiktok` | Meta 픽셀·GA4·TikTok 으로 보낼 때 쓸 그 도구의 이벤트 이름. 없으면 그 도구로 보내지 않아요 |
| `group` | `eventGroups` 의 묶음 이름 |
| `endedAt` | 더 이상 쓰지 않는 이벤트의 종료 날짜(`YYYY-MM-DD`) |
| `alerts` | `false` 면 [지표 이상 알림](/docs/alerts)의 멈춤·계측 빠짐에서 이 이벤트를 빼요. `payment_failed` 같은 오류 이벤트처럼 0 이 되는 것이 좋은 소식인 이벤트에 써요 |

## 속성 필드

| 필드 | 설명 |
|---|---|
| `enum` | 허용하는 값 목록(배열 또는 `"$enums 이름"`) |
| `optional` | 값 목록 속성을 비워도 될 때 `true`. `order` 의 맨 끝에만 둘 수 있어요 |
| `column` | 숫자나 자유 입력 글자를 저장할 위치. 숫자는 `double1`, 글자는 `blob8`(32자)·`blob9`(64자) |
| `tools` | `"only"` 면 광고·분석 도구로만 보내고, `"never"` 면 allob 에만 보내요 |

값 목록 속성은 `order` 순서대로 `/` 로 이어져 하나로 저장되고, 32자를 넘으면 잘려요. 짧은 값을 쓰세요.

## tracking 필드

| 필드 | 설명 |
|---|---|
| `landEvent` | 광고 링크로 들어온 순간에 남길 이벤트. 기본 `ad_land` |
| `surface` | 이벤트의 기본 `surface`(`web`·`app`) |
| `allowedHosts` | 측정할 도메인 목록. 목록 밖에서는 보내지 않아요 |
| `metaPixel`·`tiktokPixel`·`ga4` | 광고·분석 도구 ID(`{ "id": … }`, GA4 는 `{ "measurementId": … }`). 비우면 그 도구는 꺼져요 |
| `consent` | 지역별 동의 방식. [웹사이트에 측정 넣기](/docs/web#동의-받기) 참고 |
| `category` | 값 목록 속성을 저장하는 방식: `separator`(기본 `/`), `maxLength`(검사할 최대 길이 — 32 를 적으면 가장 긴 조합을 미리 검사해요), `unknown`(값 목록 밖 값 대신 넣을 글자. 있으면 `server.ts` 에 `known()` 이 생겨요) |
| `session` | 앱 세션 기준(백그라운드에서 얼마나 지나면 새 세션인지) |
| `transport` | 이벤트를 모아 보내는 방식 |

## apps 필드

`apps.measurement` 는 앱 측정 모듈(`allob sdk pull` 의 `native` 대상 — [앱에 측정 넣기](/docs/app#광고분석-도구google-애널리틱스meta-붙이기))이 쓰는 값이에요. 비밀 값은 적지 않아요.

| 필드 | 설명 |
|---|---|
| `google` | `true` 면 Google 애널리틱스(Firebase)를 켜요. 앱 폴더에 Firebase 설정 파일이 있어야 실제로 켜져요. 기본은 꺼짐 |
| `meta` | Meta 앱 이벤트 `{ "appId": "<앱 ID 숫자>", "clientTokenEnv": "<환경 변수 이름>" }`. 클라이언트 토큰은 빌드 환경 변수에만 넣고, 여기엔 그 이름만 적어요 |
| `att` | iOS 앱 추적 허용 창 문구 `{ "text": { "en": "…", "ko": "…" } }`. `google`·`meta` 를 켜면 `en` 이 꼭 있어야 해요(문구 없이 창을 띄우면 iOS 가 앱을 멈춰요) |
| `skAdNetwork` | 광고 네트워크 ID 목록(`"cstr6suwn9.skadnetwork"` 꼴) |
| `storeKitConsumables` | 소모품 결제가 있으면 `true`(iOS 18+ 에서 결제 기록에 소모품도 남게) |
| `admob` | AdMob 광고를 싣는 앱 `{ "mediation": ["applovin", …] }`. Google 이 권하는 SKAdNetwork 목록과 고른 미디에이션 어댑터가 붙어요. 고를 수 있는 네트워크: applovin·unity·mintegral·pangle·liftoff·inmobi·dtexchange |
| `privacyManifest` | iOS privacy manifest(`NSPrivacyTracking`·`NSPrivacyTrackingDomains`·`NSPrivacyCollectedDataTypes`·`NSPrivacyAccessedAPITypes`). 도구를 켰을 때만 앱에 들어가요 |

여기서 켠 업체(Google 애널리틱스·Meta·AdMob·미디에이션 네트워크)는 [법 문서](/docs/legal) 사실에 바로 들어가요.

## 표준 이벤트

스키마에 적지 않아도 항상 쓸 수 있는 이벤트예요. 웹 SDK 에 같은 이름의 함수가 있어요.

| 이벤트 | 언제 | 웹 SDK 함수 |
|---|---|---|
| `ad_land` | utm 이 붙은 광고 링크로 들어왔을 때(SDK 가 자동으로) | 없음 |
| `scroll_depth` | 페이지를 25·50·75·90% 내렸을 때 | `watchScrollDepth()` |
| `lead` | 연락처나 대기 명단을 남겼을 때 | `lead()` |
| `cta_click` | 주요 버튼을 눌렀을 때 | `ctaClick()` |
| `signup_start`·`signup_complete` | 가입을 시작했을 때, 마쳤을 때 | `signUpStart()`·`signUpComplete()` |
| `login` | 로그인했을 때 | `login()` |
| `purchase` | 결제했을 때. allob 기록은 서버가 남기고, 브라우저는 광고·분석 도구로만 보내요 | `purchase(value, transactionId)` |

## 버전과 호환성

- 이벤트나 속성을 추가하는 건 언제든 괜찮아요. 이미 배포된 SDK 는 새 이벤트를 보내지 않을 뿐이에요.
- 이벤트 이름을 바꾸거나 값의 뜻을 바꾸면 이전 숫자와 이어지지 않아요. 새 이름으로 추가하고, 이전 이벤트에는 `endedAt` 을 적어 두세요.
- `schemaVersion` 이 지금 CLI 보다 새로우면 `allob update` 를 하라고 알려 줘요.
