배포와 인프라8 분 읽기

앱 심사원이 내 퍼널에 신규 유저로 집계됐다

제출 5분 뒤, 아직 공개되지 않은 버전에서 두 대의 기기가 앱을 열었습니다. 심사 기기는 신규 유저와 구별되지 않고, 하필 측정하려던 지표를 아래로만 끌어내립니다. 걸러내는 대신 드러내기로 했습니다.

#app-store#analytics#ios#android
개념 도식: 퍼널 맨 위 칸에 사람 아이콘 여러 개 중 두 개가 돋보기 아이콘. 아래 칸으로 갈수록 사람이 줄고 돋보기는 첫 칸에 멈춰 있음.
심사 기기 2대는 첫 칸에서 멈춘다. 신규의 2.4%가 전환율 분모에만 들어앉는다.

첫 세션 퍼널 계측을 넣은 빌드를 App Store에 제출했습니다. 앱을 열었는지, 위치 권한을 어떻게 처리했는지, 걷기를 시작했다가 실패했는지를 기기 단위로 세는 이벤트 8개입니다.

제출 직후에 확인 삼아 서버 테이블을 조회했습니다. 심사 통과 전이니 비어 있어야 합니다.

first_open  install b4c53bc9  ios  1.5.1  07:03:05Z
app_open    install b4c53bc9  ios  1.5.1  07:03:05Z
first_open  install 40da83f2  ios  1.5.1  07:04:06Z
app_open    install 40da83f2  ios  1.5.1  07:04:06Z

제출은 06:58Z였습니다. 5분 뒤에 두 대의 기기가 아직 공개되지 않은 버전으로 앱을 열었습니다.

심사 기기는 실사용자 기기와 구별되지 않는다

1.5.1은 심사 대기 상태입니다. TestFlight 외부 테스터도 없습니다. 제 기기에서 실행하지도 않았습니다. 그런데 서로 다른 install_id 두 개가 1분 간격으로 앱을 켰습니다.

App Review가 앱을 실행한 것입니다.

당연한 얘기인데 계측 설계에서는 놓치는 지점입니다. 심사는 사람이 하거나 자동화가 하는데, 어느 쪽이든 그들의 기기는 실사용자 기기와 완전히 동일하게 보입니다. 기기 단위 식별자를 쓰면 더 그렇습니다 — 새 설치, 첫 실행, 그 다음 이벤트까지 신규 유저의 궤적과 구별되지 않습니다.

규모를 생각하면 왜 문제인지가 분명해집니다. 이 앱의 iOS 28일 설치는 85건입니다. 심사 기기 2대는 **신규의 2.4%**입니다. 릴리스를 자주 하는 편이라 심사도 자주 받습니다. 그리고 심사 기기는 정의상 걷기를 하지 않습니다 — 밖에 나가서 폐곡선을 만들 이유가 없으니까요. 즉 측정하려던 바로 그 지표(첫 걷기 도달률)를 아래로만 끌어내리는 방향으로 기여합니다.

전환율 분모에 조용히 들어앉아 결론을 한 방향으로 왜곡하는 종류의 오염입니다.

당신이라면 이걸 어떻게 처리하시겠습니까? 필터를 걸어 조용히 지우겠습니까, 아니면 표에 남겨두고 매번 보이게 하겠습니까?

걸러내기가 아니라 드러내기

저는 후자를 골랐습니다. 리더 도구가 항상 플랫폼·버전 분해를 함께 찍게 했습니다.

select platform, coalesce(app_version,'?') app_version,
       count(distinct install_id) installs, count(*) events,
       min(created_at) first_seen
  from funnel_events
 where created_at > now() - interval '28 days'
 group by 1,2 order by 3 desc;

출력 위에 이 한 줄을 같이 인쇄합니다.

플랫폼·버전 분해 (공개 전 버전이 보이면 심사 트래픽이다)

숫자를 조용히 고치는 대신, 보는 사람이 매번 판단하게 만드는 쪽입니다. 조용히 필터를 걸면 나중에 그 필터가 뭘 지웠는지 아무도 모릅니다.

Android 쪽은 같은 문제가 다르게 나타납니다. Play는 심사 게이트가 약해서 대신 내부 트랙 테스터가 같은 역할을 합니다. 여기도 버전으로 구분되므로 같은 규칙이 통합니다.

지금 바로 점검할 3가지

  1. 지난 28일 퍼널을 앱 버전별로 쪼개 보세요. 아직 공개 안 된 버전이 보이면 그게 심사 트래픽입니다.
  2. 신규 유저 분모에 들어가는 오염원을 세어 보세요 — 심사, 테스트 스위트, 스크린샷 촬영, 내부 트랙. 몇 %입니까?
  3. 지금 걸려 있는 조용한 필터가 있습니까? 그 필터가 지운 행 수를 출력에 같이 찍고 있습니까?

솔직한 부분

이걸 미리 설계하지 않았습니다. 스크린샷 모드 오염과 테스트 오염은 가드를 넣었는데, 심사는 생각조차 못 했습니다.

그리고 완전히 걸러낼 방법이 없습니다. 심사 기기를 식별하는 신호가 없습니다. 지금 쓰는 건 프록시 하나입니다 — "공개되지 않은 버전에서 온 이벤트는 심사다." 그런데 승인 직후에는 그 버전이 공개 버전이 되므로 승인 시점 전후를 사람이 구분해줘야 합니다. 자동으로 안 됩니다. IP나 기기 모델로 걸러볼 수도 있겠지만 그건 수집하지 않습니다. 퍼널 하나 보려고 개인정보 표면을 늘리는 건 교환이 나쁩니다. 승인 뒤 며칠은 데이터를 그냥 못 쓴다고 보는 게 맞습니다.

부수적으로, 이 사고 덕에 좋은 게 하나 생겼습니다. 심사가 앱을 실행한다는 건 계측 체인이 살아 있다는 종단 증명입니다. 승인 전에 "RPC가 실제로 붙는가"를 공짜로 확인했습니다. Android는 에뮬레이터로 확인했지만 iOS는 심사 통과 전까지 실기기 확인 수단이 없었는데, 심사가 그걸 대신해준 셈입니다.

이 계측으로 얻은 첫 7일 퍼널과 그 주변 플릿 숫자는 113일 결제 원장 글에 있습니다.

다음 릴리스를 제출한 직후, 이벤트 테이블을 한 번 조회해 보시겠습니까? 비어 있어야 할 자리에 사람이 있을지도 모릅니다.

유튜브

제출 5분 뒤 아직 공개 안 된 버전으로 기기 두 대가 앱을 열었다

관련 글