# 플래그 노출을 분석 도구로 보내기

비율 롤아웃이나 타기팅 규칙은 각 사용자가 받을 변형을 정합니다. 변형 B가 A보다 전환이 잘 되는지 알려면 분석 도구에 두 가지가 필요합니다. 각 사용자가 어떤 변형을 봤는지(**노출**)와 그 뒤에 무엇을 했는지(구매 같은 **결과**)입니다. Mocco의 OpenFeature 프로바이더는 둘 다 여러분이 작성한 함수에 넘겨 주고, 여러분은 이미 쓰고 있는 도구로 전달합니다. 도구가 사용자 기준으로 둘을 묶어 결과를 계산합니다. Mocco는 이 이벤트를 받지 않습니다.

이 가이드는 서버라면 [기능 플래그 빠른 시작](https://www.mocco.club/docs/ko/articles/f3cruw-quickstart), 웹 페이지와 앱이라면 [브라우저와 앱에서 기능 플래그 쓰기](https://www.mocco.club/docs/ko/articles/nb48sq-browsers-and-apps)처럼 코드에 플래그가 이미 연결되어 있다고 가정합니다.

## 1. 프로바이더에 `onExposure` 추가하기

세 프로바이더 모두 같은 세 가지 옵션을 받습니다.

```ts
import { MoccoWebProvider } from '@mocco/openfeature-web';
import { OpenFeature } from '@openfeature/web-sdk';

await OpenFeature.setProviderAndWait(
  new MoccoWebProvider({
    publishableKey: 'mk_pub_…',
    onExposure: event => sendToAnalytics(event),
    exposureAttributes: ['plan'],
  }),
);
```

| 옵션 | 하는 일 |
|---|---|
| `onExposure` | 여러분의 함수입니다. 노출과 결과 이벤트를 받습니다(아래 참고). |
| `exposureAttributes` | 각 이벤트에 복사할 평가 컨텍스트 속성입니다. 예: `plan`, `country`. 컨텍스트의 다른 값은 복사하지 않습니다. 기본값: 없음. |
| `exposureCacheSize` | 중복을 피하려고 프로바이더가 기억하는 (플래그, 변형, 사용자) 조합의 수입니다. 기본값: 10,000. |

`MoccoProvider`(`@mocco/openfeature-server`)와 `MoccoReactNativeProvider`(`@mocco/openfeature-react-native`)도 같은 옵션을 받습니다.

## 2. 함수가 받는 것

**노출**은 다음과 같습니다.

```ts
{
  type: 'exposure',
  flagKey: 'new-checkout',
  variant: 'b',
  reason: 'TARGETING_MATCH',      // 또는 'SPLIT'
  targetingKey: 'user_8f3a…',     // 컨텍스트의 targetingKey
  environment: 'production',      // 지금은 서버에서만
  rulesetVersion: 42,             // 지금은 서버에서만
  timestamp: '2026-11-09T09:00:00.000Z',
  attributes: { plan: 'pro' },    // exposureAttributes에 적은 것만
}
```

언제 노출로 셀까요?

- **규칙이나 비율 롤아웃이 변형을 골랐을 때만.** 모두에게 같은 값을 주는 플래그, 꺼진 플래그, 코드의 기본값, 오류, 없는 플래그는 세지 않습니다. 누구도 어떤 변형에 배정되지 않았기 때문입니다.
- **코드가 플래그를 읽을 때만.** 브라우저와 앱에서는 프로바이더가 모든 플래그를 한 번에 가져오지만, 가져오는 것만으로는 아무것도 세지 않습니다. 페이지나 화면이 플래그를 요청할 때가 사용자가 변형을 볼 수 있는 때이므로 그때 노출이 일어납니다.
- **플래그, 변형, 사용자마다 한 번.** 프로바이더가 그 조합을 기억하는 동안 다음 읽기는 건너뜁니다. 기억하는 범위는 서버 프로세스, 페이지, 앱 세션 하나입니다. 분석 도구가 한 번 더 중복을 걸러도 괜찮습니다.
- **저장해 둔 답도 셉니다.** 페이지나 앱이 지난번에 저장한 답으로 시작하면(OpenFeature가 이유를 `CACHED`로 보고합니다) 규칙이 고른 변형이라면 읽기를 노출로 세고, 이벤트에는 원래의 이유가 담깁니다.

`environment`와 `rulesetVersion`은 프로바이더가 아는 값입니다. 서버 프로바이더는 평가하는 룰셋에서 읽습니다. 브라우저와 앱은 답만 받고 그 답에는 아직 이 값이 없으므로 `undefined`입니다. 보고서에 필요하면 환경을 직접 넣으세요(환경마다 다른 키로 빌드하니까요).

**결과**는 OpenFeature의 추적 호출에서 옵니다.

```ts
OpenFeature.getClient().track('checkout_completed', { value: 49.0, currency: 'USD' });
```

그러면 함수는 다음을 받습니다.

```ts
{
  type: 'track',
  name: 'checkout_completed',
  targetingKey: 'user_8f3a…',
  value: 49,
  details: { currency: 'USD' },
  timestamp: '2026-11-09T09:01:12.000Z',
  attributes: { plan: 'pro' },
}
```

앱이 이미 전환을 분석 도구로 직접 보내고 있다면 `track`은 필요 없습니다. 같은 함수로 둘 다 보낼 수 있게 있는 것입니다.

함수가 플래그 평가를 망가뜨릴 수는 없습니다. 함수가 예외를 던지거나 실패하는 프로미스를 돌려주면 그 오류는 버려지고 플래그 값은 평소처럼 제공됩니다.

## 3. 보내기 전에 지킬 두 가지

**타기팅 키는 분석 도구의 사용자 ID여야 합니다.** 분석 도구는 사용자 기준으로 노출과 결과를 묶습니다. 플래그의 `targetingKey`가 `user_8f3a`인데 도구는 그 사용자를 `42`로 알고 있다면 둘은 만나지 않고 실험에는 아무것도 나오지 않습니다. 둘에 같은 ID를 쓰세요. 분석 도구에서 사용자를 식별할 때 `targetingKey`로 넘기는 값을 쓰면 됩니다. 로그인하지 않은 방문자는 도구의 익명 ID(예: PostHog의 `get_distinct_id()`)를 `targetingKey`로 쓰세요. 이 경우 방문자가 로그인해서 새 ID를 받으면 다른 변형에 들어갈 수 있습니다.

**보낼 것만 보내세요.** 평가 컨텍스트 전체에는 이메일 주소, IP 주소, 아직 발표하지 않은 요금제가 들어 있을 수 있으므로 프로바이더는 컨텍스트를 통째로 복사하지 않습니다. `exposureAttributes`에 적은 속성만 넘어갑니다. `targetingKey`에는 이메일 주소 대신 내부 사용자 ID 같은 가명 ID를 쓰고, 플래그 값보다 변형 이름을 보내세요. 값에는 공개하고 싶지 않은 문구가 들어 있을 수 있습니다.

## 4. 레시피

분석 도구 레시피는 그대로 쓸 수 있는 `onExposure` 함수입니다. Datadog과 Sentry는 세션과 오류 옆에 플래그를 보여 주므로 모든 평가가 필요하고, 대신 OpenFeature 훅을 씁니다. `posthog` 같은 이름은 그 도구에 맞게 이미 설정해 둔 클라이언트를 뜻합니다.

### PostHog

PostHog은 `$feature_flag_called` 이벤트를 실험의 노출로 봅니다.

```ts
onExposure: event => {
  if (event.type === 'exposure') {
    posthog.capture('$feature_flag_called', {
      $feature_flag: event.flagKey,
      $feature_flag_response: event.variant,
    });
  } else {
    posthog.capture(event.name, { value: event.value, ...event.details });
  }
},
```

브라우저에서는 `targetingKey`로 넘기는 것과 같은 ID로 `posthog.identify(user.id)`를 호출하세요. 서버에서는 프로세스 하나가 여러 사용자를 처리하므로 `posthog-node`에 사용자를 직접 넘깁니다.

```ts
onExposure: event => {
  if (event.type === 'exposure' && event.targetingKey !== undefined) {
    posthog.capture({
      distinctId: event.targetingKey,
      event: '$feature_flag_called',
      properties: { $feature_flag: event.flagKey, $feature_flag_response: event.variant },
    });
  }
},
```

### Amplitude

Amplitude Experiment는 플래그와 변형이 담긴 `$exposure` 이벤트를 노출로 세고, 사용자 속성에 변형을 더해 이후 이벤트를 변형별로 나눠 볼 수 있게 합니다.

```ts
onExposure: event => {
  if (event.type === 'exposure') {
    amplitude.track('$exposure', { flag_key: event.flagKey, variant: event.variant });
  } else {
    amplitude.track(event.name, { value: event.value, ...event.details });
  }
},
```

`targetingKey`와 같은 ID로 `amplitude.setUserId(user.id)`를 설정하세요.

### Mixpanel

Mixpanel의 실험 보고서는 `$experiment_started` 이벤트를 읽습니다.

```ts
onExposure: event => {
  if (event.type === 'exposure') {
    mixpanel.track('$experiment_started', {
      'Experiment name': event.flagKey,
      'Variant name': event.variant,
    });
  } else {
    mixpanel.track(event.name, { value: event.value, ...event.details });
  }
},
```

`targetingKey` 값으로 `mixpanel.identify(user.id)`를 호출하세요.

### Google Analytics 4

GA4에는 사용자가 실험 변형에 들어갔을 때 쓰는 권장 이벤트 `experience_impression`이 있고, 변형은 `exp_variant_string`에 담습니다.

```ts
onExposure: event => {
  if (event.type === 'exposure') {
    gtag('event', 'experience_impression', {
      exp_variant_string: `mocco-${event.flagKey}-${event.variant}`,
    });
  } else {
    gtag('event', event.name, { value: event.value, ...event.details });
  }
},
```

`gtag('config', …)` 호출의 `user_id`를 `targetingKey` 값으로 설정하세요. GA4는 통계를 직접 계산하지 않으므로 탐색 분석에서 변형을 비교하거나 BigQuery로 내보내세요.

### Segment

Segment 명세는 노출을 `Experiment Viewed`라고 부릅니다. Amplitude, Mixpanel, 데이터 웨어하우스 같은 대상이 알아서 대응시킵니다.

```ts
onExposure: event => {
  if (event.type === 'exposure') {
    analytics.track('Experiment Viewed', {
      experiment_id: event.flagKey,
      variation_id: event.variant,
    });
  } else {
    analytics.track(event.name, { value: event.value, ...event.details });
  }
},
```

`targetingKey` 값으로 `analytics.identify(user.id)`를 호출하세요.

### OpenTelemetry

OpenTelemetry 시맨틱 규약에는 `feature_flag.evaluation` 이벤트가 있습니다. 로그 레코드로 내보냅니다.

```ts
import { logs } from '@opentelemetry/api-logs';

const logger = logs.getLogger('mocco-flags');

onExposure: event => {
  if (event.type === 'exposure') {
    logger.emit({
      eventName: 'feature_flag.evaluation',
      attributes: {
        'feature_flag.key': event.flagKey,
        'feature_flag.result.variant': event.variant,
        'feature_flag.result.reason': event.reason.toLowerCase(),
        'feature_flag.provider.name': 'Mocco',
        ...(event.targetingKey !== undefined && { 'feature_flag.context.id': event.targetingKey }),
        ...(event.rulesetVersion !== undefined && { 'feature_flag.version': String(event.rulesetVersion) }),
      },
    });
  }
},
```

이 방법은 첫 노출을 기록합니다. 트레이스나 메트릭을 위해 모든 평가를 기록하려면 OpenFeature의 `js-sdk-contrib` 저장소에 있는 OpenTelemetry 훅 같은 OpenFeature 훅을 추가하세요. 이 훅은 Mocco를 포함한 어떤 프로바이더와도 함께 동작합니다.

### Datadog RUM

Datadog RUM은 플래그 값을 현재 뷰와 그 뷰의 오류에 붙이므로 세션과 오류를 변형별로 걸러 볼 수 있습니다. 뷰마다 그 뷰에서 읽은 플래그가 필요하고 사용자가 처음 만난 순간만으로는 부족하므로, `onExposure` 대신 모든 평가에서 실행되는 OpenFeature 훅을 씁니다.

```ts
import { OpenFeature } from '@openfeature/web-sdk';

OpenFeature.addHooks({
  after: (hookContext, details) => {
    if (details.variant !== undefined) {
      datadogRum.addFeatureFlagEvaluation(hookContext.flagKey, details.variant);
    }
  },
});
```

RUM을 초기화할 때 사용하는 SDK 버전의 Datadog 설정 가이드에 따라 기능 플래그 수집을 켜고, `targetingKey`와 같은 ID로 `datadogRum.setUser({ id: user.id })`를 설정하세요. 이 훅은 값이 아니라 변형 이름을 보냅니다.

### Sentry

Sentry는 최근 플래그 평가를 각 오류에 붙여 플래그로 이슈를 검색할 수 있게 합니다. Datadog처럼 첫 노출이 아니라 모든 평가가 필요하며, 이를 위한 자체 OpenFeature 훅을 제공합니다. `onExposure` 대신 Mocco 프로바이더 옆에 추가하세요.

```ts
import * as Sentry from '@sentry/browser';
import { OpenFeature } from '@openfeature/web-sdk';

Sentry.init({ dsn: '…', integrations: [Sentry.openFeatureIntegration()] });
OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook());
```

서버나 React Native 앱은 해당 SDK의 Sentry 기능 플래그 가이드를 따르세요. Sentry는 불리언 플래그 값만 기록하므로 문자열이나 숫자 변형을 가진 플래그는 나타나지 않습니다.

## 자주 묻는 질문

**Mocco가 이 이벤트를 보나요?** 아니요. Mocco는 플래그별 평가 횟수만 보관하고, 그 횟수로 [오래된 플래그](https://www.mocco.club/docs/ko/articles/e72bxg-stale-flags)를 알려 줍니다. 노출과 결과는 여러분의 함수가 보내는 곳으로만 갑니다.

**일부 플래그의 노출만 보내고 싶어요.** 함수에서 `event.flagKey`를 확인하세요. 예를 들어 실험 중인 플래그 목록과 비교하면 됩니다.

**비율 롤아웃이 없어도 노출이 생기나요?** 네. "요금제가 pro" 처럼 일부 사용자에게 변형을 고르는 규칙이라면 무엇이든 셉니다. 모두에게 하나의 값을 주는 플래그는 비교할 대상이 없으므로 세지 않습니다.
