플래그 노출을 분석 도구로 보내기
비율 롤아웃이나 타기팅 규칙은 각 사용자가 받을 변형을 정합니다. 변형 B가 A보다 전환이 잘 되는지 알려면 분석 도구에 두 가지가 필요합니다. 각 사용자가 어떤 변형을 봤는지(노출)와 그 뒤에 무엇을 했는지(구매 같은 결과)입니다. Mocco의 OpenFeature 프로바이더는 둘 다 여러분이 작성한 함수에 넘겨 주고, 여러분은 이미 쓰고 있는 도구로 전달합니다. 도구가 사용자 기준으로 둘을 묶어 결과를 계산합니다. Mocco는 이 이벤트를 받지 않습니다.
이 가이드는 서버라면 기능 플래그 빠른 시작, 웹 페이지와 앱이라면 브라우저와 앱에서 기능 플래그 쓰기처럼 코드에 플래그가 이미 연결되어 있다고 가정합니다.
1. 프로바이더에 onExposure 추가하기
세 프로바이더 모두 같은 세 가지 옵션을 받습니다.
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. 함수가 받는 것
노출은 다음과 같습니다.
{
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의 추적 호출에서 옵니다.
OpenFeature.getClient().track('checkout_completed', { value: 49.0, currency: 'USD' });그러면 함수는 다음을 받습니다.
{
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 이벤트를 실험의 노출로 봅니다.
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에 사용자를 직접 넘깁니다.
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 이벤트를 노출로 세고, 사용자 속성에 변형을 더해 이후 이벤트를 변형별로 나눠 볼 수 있게 합니다.
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 이벤트를 읽습니다.
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에 담습니다.
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, 데이터 웨어하우스 같은 대상이 알아서 대응시킵니다.
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 이벤트가 있습니다. 로그 레코드로 내보냅니다.
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 훅을 씁니다.
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 프로바이더 옆에 추가하세요.
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는 플래그별 평가 횟수만 보관하고, 그 횟수로 오래된 플래그를 알려 줍니다. 노출과 결과는 여러분의 함수가 보내는 곳으로만 갑니다.
일부 플래그의 노출만 보내고 싶어요. 함수에서 event.flagKey를 확인하세요. 예를 들어 실험 중인 플래그 목록과 비교하면 됩니다.
비율 롤아웃이 없어도 노출이 생기나요? 네. "요금제가 pro" 처럼 일부 사용자에게 변형을 고르는 규칙이라면 무엇이든 셉니다. 모두에게 하나의 값을 주는 플래그는 비교할 대상이 없으므로 세지 않습니다.