도메인: Analytics (Firebase Analytics 이벤트)
룰
| 항목 | 값 |
|---|---|
| SDK | firebase_analytics (Flutter) |
| Firebase 프로젝트 | dartbrief (dev/prod 단일 — 프로젝트 분리 X) |
| Dev 격리 | setAnalyticsCollectionEnabled(flavor.isProd) — dev 빌드는 수집 OFF |
| 사용자 식별자 | setUserId(user.userId.toString()) — auth/me 성공 시 |
왜 단일 Firebase 프로젝트: Google 로그인 / FCM / App Check 가 모두 같은 GoogleService-Info.plist 에 묶여 있어 dev/prod 프로젝트 분리는 마찰만 큼. dev 메트릭 오염은 collection 토글로 처리.
도입 배경 (2026-05-07 5명 패널 토론)
lazy auth (비로그인 reveal 게이트) + 웹 부활 검토 중, DEVIL 의 핵심 지적:
"전환 측정 없이 lazy 풀면 게이트 통과율 / 중도 이탈을 못 봄. 출시 후 회복 불가능한 데이터 공백."
→ 5-PR 계획 1번으로 analytics 사전 셋업 (PR #54 머지 완료, 2026-05-07).
단일 진입점
mobile/lib/shared/analytics/analytics_service.dart 의 AnalyticsService 만 사용. 호출 측은 typed 메서드만 — 이벤트명/파라미터 키 문자열이 코드 전반에 흩어지지 않도록.
final analytics = ref.read(analyticsServiceProvider);
await analytics.logRevealAttempt(rceptNo: rceptNo);모든 메서드는 try/catch 로 감싸 native 채널 실패 시에도 앱 흐름을 막지 않음 (분석 누락 < 사용자 액션 차단).
이벤트 taxonomy
인증 (Firebase 표준)
| 이벤트 | 메서드 | 파라미터 | 송출 시점 |
|---|---|---|---|
login | logLogin | loginMethod: 'kakao'|'google'|'apple' | AuthController.handleAuthToken 안에서 refetch() 후 (login_page 의 mounted 가드 외) |
sign_up | logSignUp | signUpMethod | 현재 미사용 — 백엔드가 신규/기존 구분 응답 안 함. 시그니처만 준비 |
logout | logLogout | — | AuthController.logout() 안에서 setUserId(null) 보다 먼저 |
setUserId 직렬화 — login 흐름은 AuthController.handleAuthToken 이 refetch() 안에서 setUserId(user.userId.toString()) 를 await 한 뒤 같은 컨트롤러에서 logLogin 직접 송출 (page-level 호출 X — LoginPage dispose/race 회피, 단일 책임 지점). 401 콜백 vs 명시적 logout race 는 _isLoggingOut 플래그로 우회 (자세히는 flutter-gotchas §7).
공시 / 분석 funnel
| 이벤트 | 파라미터 | 송출 시점 |
|---|---|---|
disclosure_view | rcept_no, corp_code? | 공시 상세 페이지 진입 |
analyze_inspect | rcept_no, status, is_short?, already_revealed? | Inspect 메타 도착 — 폴링 dedup (아래) |
reveal_attempt | rcept_no | "AI 분석 보기" 버튼 클릭 직전 |
reveal_result | rcept_no, outcome, newly_revealed?, can_watch_ad? | reveal API 응답 / 예외 / 화면 이탈 |
status 값 (analyze_inspect): 'completed' | 'pending' | 'unavailable' — Flutter AnalysisMetaState 타입 기준. 백엔드 QUOTA_EXHAUSTED 는 HTTP 200 으로 내려와 AnalysisMetaCompleted → 'completed' 로 기록됨. 'unavailable' 은 202+UNAVAILABLE 응답 시 송출 (현재 백엔드 미생성, reserved).
폴링 dedup — disclosure_detail_controller._lastInspectStatus 가 직전 status 를 기억해 같은 status 가 폴링으로 5회 들어와도 1번만 송출. status 가 바뀌면 다시 송출.
RevealOutcome enum
reveal_result.outcome 차원. 모든 종결 분기가 단일 이벤트 — funnel 분기 분석 용이.
| 값 | 의미 | 백엔드 매핑 |
|---|---|---|
success | reveal 성공 (PaidReveal 또는 FreeReveal) | Outcome.PaidReveal / FreeReveal |
quota_exhausted | 주 quota 소진 | 429 QUOTA_EXHAUSTED |
not_ready | 분석 아직 안 됨 | Outcome.Pending |
unavailable | 영구 불가 (FAILED + retry≥3) | reserved — 현재 Flutter reveal 경로 미송출 (별도 PR scope) |
error | 클라이언트 측 예외 (네트워크/서버 장애) | — |
cancelled | reveal 응답 도착 전 controller dispose | — |
error / cancelled 는 reveal_attempt 대비 reveal_result 의 구조적 누락 방지 — 모든 attempt 가 어떤 outcome 으로든 종결되어야 funnel 깨짐을 알 수 있음.
Engagement
| 이벤트 | 파라미터 | 송출 시점 |
|---|---|---|
watchlist_add | corp_code | 종목 추가 성공 (unawaited 로 화면 흐름 차단 X) |
광고 (현재 미송출)
| 이벤트 | 메서드 | 상태 |
|---|---|---|
ad_reward_earned | logAdRewardEarned | 시그니처만 유지, 클라이언트 송출 X |
왜 클라이언트 송출 안 하나 — prod 는 AdMob SSV 가 백엔드를 직접 호출하므로 클라이언트는 검증 통과 여부를 모름. 클라이언트에서 송출하면 SSV 실패해도 보상 funnel 이 +1 → false-positive. 정확한 측정은 백엔드 SSV 핸들러에서 Firebase Measurement Protocol / BigQuery 로 별도 기록 예정 (별도 PR scope).
lazy auth
| 이벤트 | 파라미터 | 의도 |
|---|---|---|
auth_gate_shown | action: 'reveal'|'watchlist'|'feedback'|'sign_in' | 게이트 노출 — 비로그인 사용자가 어떤 액션으로 게이트에 닿는지. sign_in 은 명시적 로그인 진입점 (설정 → 로그인 메뉴) 으로 후속 액션 없이 로그인 자체가 목적 |
auth_gate_dismissed | action, reason: 'close'|'swipe' | 게이트 닫힘 (취소) — 게이트 통과율 분모. reason 으로 X 명시 닫기 vs swipe down/system back 구분 → 풀스크린 vs 모달 A/B 결정 차원 |
AuthGateActionenum 으로 액션 차원 고정.- 게이트 통과 (로그인 성공) 경로는 별도 이벤트 X — 기존
login(provider)이벤트가 게이트 직후 송출돼 timestamp correlation 으로 측정. reason분리는 모달 vs 풀스크린 결정 입력값. swipe 비율이 높으면 풀스크린이 dismiss 마찰 추가 가치, close (X) 비율이 높으면 추가 가치 낮음.
Edge case
- dev 빌드 → prod 메트릭 오염:
bootstrap.dart가setAnalyticsCollectionEnabled(flavor.isProd)로 dev 는 수집 OFF. flavor 기준 (kDebugMode아님 —make run-dev가--release라kDebugMode=false). - 사용자 식별자 race —
setUserId(null)이logLogout보다 먼저 발생하면 logout 이 익명 귀속되어 이탈 funnel 깨짐.AuthController.logout()은_isLoggingOut플래그 + 순차 await 로 보장. - 401 콜백 vs logout() — 토큰 만료 시
onUnauthorized콜백이setUserId(null)호출. 그러나 명시적 logout 흐름에서는 logout 이벤트가 익명 귀속될 위험 →_isLoggingOut=true면 콜백이 setUserId(null) 우회. - Controller dispose 중 reveal:
disclosure_detail_controller.reveal()은analytics를 함수 진입 시 캡처 (ref.read를 await 후 호출하면 dispose 후 crash). dispose 후엔RevealOutcome.cancelled송출. - 폴링 중 같은 이벤트 폭주 — Inspect 메타가 pending 인 동안 폴링 (5→10→15→20s backoff, 이후 60s 고정) →
_lastInspectStatusdedup 으로 1회만 송출.
관련 코드
mobile/lib/shared/analytics/analytics_service.dart— 단일 진입점mobile/lib/app/bootstrap.dart— collection togglemobile/lib/shared/auth/auth_controller.dart— login/logout/setUserId 직렬화mobile/lib/features/disclosure/application/disclosure_detail_controller.dart— Inspect/reveal funnel + dispose 가드mobile/lib/features/watchlist/application/watchlist_controller.dart— watchlist_add
법적 페이지 영향
Firebase Analytics 는 패키지 추가만으로 App Instance ID + 자동 수집 이벤트(session, screen_view, app_open 등) + 기기/OS/언어/국가 가 Google 로 송신된다. 따라서 다음 페이지에 Google LLC (Firebase Analytics) 위탁/국외이전 명시 필수:
frontend/src/app/privacy/page.tsx— 제2조 자동 수집 정보, 제5조 위탁, 제6조 국외 이전frontend/src/app/terms/page.tsx— 제9조·제12조 외부 서비스 목록
이벤트 taxonomy 추가 (logEvent 신규) 자체는 법적 페이지 영향 없음 (이미 "앱 사용 이벤트" 로 포괄). 단, 새로운 자동 수집 식별자 / 디바이스 정보 수집 SDK 추가 시 동시 갱신.
관련 PR
- PR #54 (2026-05-07) —
firebase_analytics도입 + 핵심 funnel 이벤트 wiring - PR #71 (2026-05-11) — 법적 페이지에 Firebase Analytics 위탁 명시 (PR #54 누락분 정정)