docs(tracking): UTM 세션 유입 점검 결과를 기록한다

This commit is contained in:
Yu Sung
2026-10-14 16:16:12 +09:00
parent 86aa3d86ad
commit 3bd75f188f
@@ -0,0 +1,102 @@
# Firebase UTM 이벤트 공식 문서 대조 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | `FirebaseTracking.logUtm` 이벤트명 변경 및 기존 호출 경로 |
| 기준 변경 | staged: `ad_partner_campaign_details` → `AnalyticsEventCampaignDetails` |
| 리뷰 일자 | 2026-10-14 |
| 리뷰어 | Sisyphus |
| 기준 문서 | 사용자 점검 요청, Google/Firebase 공식 문서, `docs/sample/sample-review.md` |
| 리뷰 상태 | 문서·정적 검토 판정 완료, 실제 속성의 수집·보고 검증 미실행 |
## 2. 리뷰 목적과 범위
UTM 기반 캠페인 수집과 현재 앱의 수동 캠페인 전송이 같은 의미로 처리되는지 확인한다. 검토 대상은 `SodaLive/Sources/Tracking/FirebaseTracking.swift`, `SodaLive/Sources/App/AppDelegate.swift`, `SodaLive/Sources/App/AppState.swift` 및 SDK 버전 명세다. 사용자 요청은 점검이며 앱 코드는 수정하지 않는다. 구현 작업이 아니므로 별도 PRD·구현 계획은 만들지 않는다.
## 3. 판정 기준
이벤트 계약과 파라미터 의미, 실제 입력 매핑, 이벤트·세션·첫 설치의 귀속 범위를 각각 구분한다. 공식 문서에 없는 동작과 실행하지 않은 수집 결과를 보장하지 않는다. 기존 로직의 제약은 이벤트명 변경으로 발생한 회귀와 구분한다.
## 4. 검토한 근거
### 코드와 의존성
- `FirebaseTracking.swift:33`: 공백 제거 후 source/medium/campaign 모두가 비어 있지 않을 때만 표준 이벤트를 전송한다.
- `AppDelegate.swift:136`, `AppDelegate.swift:196`: AppsFlyer `deep_link_sub2/3/4`를 각각 source/medium/campaign으로 매핑한다. URL의 `utm_*`를 이 함수가 직접 파싱하지 않는다.
- `AppDelegate.swift:163`, `AppDelegate.swift:223`: non-organic conversion callback과 deep-link callback에서 각각 로그한다.
- `AppState.swift:88`: 캠페인 값은 앱 프로세스의 공유 상태에 남는다. 검색된 쓰기 경로는 위 두 callback이며 누락 필드를 초기화하지 않는다.
- `SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved`: Firebase iOS SDK 10.29.0, GoogleAppMeasurement 10.28.0.
- 과거 commit `f9bad015`: 표준 캠페인 이벤트가 기록되지 않았다는 이유로 커스텀 이벤트로 변경한 이력이 있다. 이 메시지는 당시 관찰이며 현재 SDK·보고서 동작의 공식 근거가 아니다.
### 공식 문서
1. [Firebase Swift Constants](https://firebase.google.com/docs/reference/swift/firebaseanalytics/api/reference/Constants#/c:FIREventNames.h@kFIREventCampaignDetails): 재참여 캠페인의 referral details 전송용 이벤트이며 source/medium/campaign 중 적어도 하나가 필요하다. 현재 세 값을 전송하는 호출은 이 요건을 충족한다.
2. [GA4 Manual report](https://support.google.com/analytics/answer/14264492?hl=en): “UTM parameters in the click URL for a website or ... campaign_details event for a mobile app.” 앱 이벤트와 웹 UTM 모두 수동 캠페인 보고서의 입력 경로다. Session manual source/medium/campaign name은 세션 시작과 연결된 차원이다.
3. [How app key events are attributed](https://support.google.com/analytics/answer/10311900?hl=en): UTM URL 예시의 `utm_source`, `utm_medium`, `utm_campaign`은 Source, Medium, Campaign에 대응한다. 첫 설치를 수동 귀속하려면 설치 직후 `campaign_details`를 보내야 하며 해당 정보로 `first_open`을 귀속한다고 명시한다. 정확한 시간 제한은 이 문서에서 제시하지 않는다.
4. [Campaigns and traffic sources](https://support.google.com/analytics/answer/11242841?hl=en): 기존 세션 중 새 캠페인 정보를 보내도 새 세션이 시작되지 않는다. 새 값은 수집된 이벤트의 이벤트 기반 귀속에 사용되고 기존 세션의 캠페인 값에 연결되지 않는다.
5. [DebugView](https://firebase.google.com/docs/analytics/debugview?platform=ios): iOS `-FIRDebugEnabled`로 원시 이벤트를 검증할 수 있다. 실제 보고서 검증은 속성의 데이터 필터도 확인해야 한다.
6. [Measurement Protocol campaign_details](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#campaign_details): 해당 이벤트 시각 이상인 이벤트에 캠페인 정보를 적용한다고 설명한다. 이벤트 자체는 Analytics 보고서와 DebugView에 표시되지 않고 BigQuery export에 포함된다고 명시한다. 이 페이지는 Measurement Protocol 계약이며 pinned iOS SDK 경로의 가시성을 직접 입증하는 근거로 일반화하지 않는다.
### 실행한 검증
| 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `git diff --cached -- SodaLive/Sources/Tracking/FirebaseTracking.swift` | 성공 | 이벤트명 한 줄 변경, 기존 표준 파라미터 유지 |
| `rg -n 'logUtm|marketingUtm' SodaLive/Sources` 및 관련 파일 읽기 | 성공 | 두 callback과 공유 상태, all-three 조건 확인 |
| 관련 파일 `git log -3 -p` | 성공 | 이전 커스텀 이벤트 도입 사유 확인 |
| 공식 문서 HTTP 조회 및 핵심 문구 대조 | 성공 | 수동 캠페인 보고, 첫 설치 조건, 세션 귀속 제약 확인 |
| 실제 기기·DebugView·GA4 속성 보고서 | 미실행 | 테스트 기기 실행 및 속성 결과 확인 없이 수집 성공을 판정하지 않음 |
| 빌드·테스트·LSP 진단 | 미실행 | 앱 코드 변경 없는 문서 기반 점검, LSP 도구 미제공 |
## 5. 발견 사항 요약
이벤트명 변경 자체의 확정 오류는 없다. 표준 수동 캠페인 경로를 사용하는 변경이다. 다만 완전한 UTM 추적 동등성에는 아래 기존 제약과 미검증 조건이 있다.
| ID | 심각도 | 상태 | 제목 | 후속 처리 |
|---|---|---|---|---|
| REV-P1-001 | Medium | 확정, 기존 로직 | 한 필드라도 비면 전체 캠페인 전송 생략 | 세 필드 필수라는 제품 정책인지 확인 후 필요 시 별도 수정 |
| REV-P1-002 | Medium | 확정, 기존 로직 | 누락 필드가 이전 캠페인 값을 재사용 | 별도 수정 시 callback별 완결된 캠페인 데이터 사용 |
| REV-P1-003 | Medium | 보류 | 실제 callback 시점·보고서 귀속·중복 전송 | 기기·속성 기반 검증 필요 |
## 6. 발견 사항 상세
### REV-P1-001: 일부 캠페인 정보의 전송 생략
`FirebaseTracking.swift:38`은 세 값 모두를 요구한다. 초기 상태에서 source=`newsletter`, medium=`email`, campaign=`""`이면 이벤트를 보내지 않는다. Firebase 계약은 세 필드 중 하나 이상이면 가능하므로 앱 조건이 더 엄격하다. 이는 기존 조건이며 이벤트명 변경의 회귀는 아니다. 모든 링크가 세 필드를 반드시 포함한다는 정책이면 유지 가능하다. 부분 입력도 수집해야 한다면 별도 승인된 수정에서 비어 있지 않은 파라미터만 보내도록 변경하고 회귀 검증한다.
### REV-P1-002: 이전 캠페인과 새 캠페인의 혼합
`AppDelegate.swift:196`의 각 조건부 대입은 누락된 필드를 비우지 않는다. 같은 프로세스에서 캠페인 A의 세 값이 저장된 뒤 B callback이 source만 제공하면, B source와 A medium/campaign으로 이벤트를 보낸다. 입력이 없는 callback도 기존 세 값이 있으면 이전 캠페인을 다시 보낼 수 있다. 정적 코드로 확인한 기존 데이터 정합성 문제이며 기기 재현은 하지 않았다. 수정한다면 먼저 별도 계획에 회귀 Task를 추가하고 이번 callback의 값만으로 캠페인을 구성해야 한다.
### REV-P1-003: 귀속 시점과 이벤트 가시성의 실환경 검증
첫 설치 귀속은 설치 직후 전송 조건이 있다. 기존 세션 중 전송과 새 세션 시작 시 전송의 결과를 동일하게 취급할 수 없다. 두 callback의 존재만으로 실제 중복 발생을 확정하지 않는다. 담당은 앱 개발자·Analytics 속성 관리자이며, 고유 캠페인 값을 사용한 최초 설치와 재참여 테스트 결과 확보 시 재개한다. Measurement Protocol은 `campaign_details` 자체를 보고서와 DebugView에 표시하지 않는다고 명시하므로 이벤트 이름의 표시 여부와 캠페인 보고 차원 반영을 구분해야 한다. 일반 SDK 경로에서 이 이벤트가 항상 표시되거나 항상 숨겨진다는 포괄적 규칙은 이번 문서 근거로 확정하지 않는다. SDK 로그·오류와 실제 Manual report 결과를 함께 검증한다.
## 7. 확정 항목의 plan·goal 전환
이번 요청은 점검이므로 구현 Task로 전환하지 않는다. REV-P1-001은 입력 정책 확인이 먼저 필요하며, REV-P1-002 수정은 별도 승인과 PRD·계획 수립 후 수행한다. 앱 코드와 사용자의 staged 변경은 보존한다.
## 8. 리뷰 종료 판정
**최종 결론:** 이벤트명과 표준 파라미터 선택은 공식 수동 앱 캠페인 수집 방식에 맞다. `utm_*` 문자열 키를 그대로 저장하는 방식이 아니라 대응하는 source/medium/campaign 정보를 캠페인 귀속에 제공하는 방식이다. 모든 입력·전송 시점에서 세션·첫 사용자·기존 커스텀 이벤트 보고가 동일하다고 보장할 수는 없다.
**남은 검증:** 실제 링크 설정의 `deep_link_sub2/3/4` 의미와 값, 설치 직후 callback 시점, DebugView/SDK 로그의 캠페인 처리·오류, GA4 Manual report의 Session manual source/medium/campaign name, 첫 설치의 `first_open` 귀속, callback 중복 여부. 일반 이벤트 목록 확인만으로 캠페인 귀속 검증을 대체하지 않는다.
## 9. 수정 후 검증 기록
앱 코드 수정 없음. 공식 문서와 정적 코드 대조 결과만 기록한다. 실제 수집·보고 성공, 빌드 성공을 주장하지 않는다.
### 후속 의도 확인 및 판정 보강 — 2026-10-14
- 사용자 결정: source/medium/campaign 세 값이 모두 있어야 전송하는 정책을 유지한다. REV-P1-001은 동작 관찰 자체는 맞지만 사용자 요구사항상 결함이 아니며 수정 대상에서 제외한다. 현재 검사는 공백 제거 후 비어 있는지만 판정하며 값의 의미적 정확성을 검증하지는 않는다.
- 사용자 목적: GA와 AppsFlyer에서 트래픽을 세션 소스/매체 기준으로 분석하고 비교할 수 있도록 한다.
- [Google traffic-source dimensions](https://support.google.com/analytics/answer/11242870?hl=en): Session source/medium과 Session manual source/medium을 구분해야 한다. 자동 태깅과 수동 태깅이 함께 있으면 통합 유입 차원에 자동 태깅 값이 사용될 수 있으므로 수동 입력값과 항상 같다고 보장하지 않는다.
- [AppsFlyer iOS conversion data](https://dev.appsflyer.com/hc/docs/conversion-data-ios): `onConversionDataSuccess`는 앱 열기·포그라운드 복귀 시 호출되며, 해당 설치의 attribution data를 반환한다. 최초 반환 데이터는 캐시되어 후속 호출에서도 동일하다. 이는 이번 세션에서 새 링크를 클릭했다는 증거가 아니다.
- 추가 코드 확인: `SodaLiveApp.swift:72`에서 활성화 시 `AppsFlyerLib.shared().start()`를 호출한다. `AppDelegate.swift:129`는 Non-organic 여부만 검사하고 `is_first_launch`를 구분하지 않은 채 `FirebaseTracking.logUtm()`을 호출한다.
- 추가 위험 사례: 광고 A로 설치한 사용자가 나중에 링크 없이 앱을 열어도 conversion callback에서 설치 A의 값이 다시 캠페인으로 전송될 수 있다. 이 경로만으로 현재 세션의 실제 유입 링크를 식별할 수 없다. GA에서 그 값이 실제 세션 차원에 어떻게 처리됐는지는 실측 전이므로 확정하지 않는다.
- 플랫폼 경계: 변경된 함수는 Firebase `Analytics.logEvent`만 호출하므로 AppsFlyer 보고서의 귀속 자체를 바꾸지 않는다. GA로 보내는 `deep_link_sub2/3/4`가 AppsFlyer 보고서의 실제 유입·캠페인 필드와 같은 의미인지 링크 설정과 보고서 기준을 별도로 대조해야 한다.
- 보강 결론: 표준 캠페인 이벤트로 바꾸는 선택은 GA 캠페인 분석 목적에 맞다. 다만 현재 호출 로직까지 포함하면 모든 세션을 현재 유입 소스/매체로 정확하게 분류하거나 두 플랫폼의 수치가 일치하도록 하는 목적이 충족됐다고 판정할 수 없다. 최초 설치 정보와 재참여 링크를 구분하고, 새 세션 유입·세션 중 링크·링크 없는 재방문을 각각 검증해야 한다. 코드 수정은 수행하지 않는다.
- 추가 범위 확정: 사용자는 세션 유입의 정상 확인만 필요하다고 명시했다. 첫 사용자·설치 귀속 및 GA/AppsFlyer 수치 일치는 완료 기준에서 제외한다. 남은 판정은 실제 유입 링크로 시작한 새 세션의 소스/매체 반영이며, 캐시된 설치 정보를 재전송하는 기존 callback 경로와 전송 시점을 확인해야 한다.
- 검증 화면: [GA4 Traffic acquisition report](https://support.google.com/analytics/answer/12923437?hl=en)의 Session source / medium과 Manual report의 Session manual source / medium을 함께 확인한다. 새 세션 유입값과 예상값이 일치하는지 실측하지 않았으므로 이벤트명 적합성과 세션 분석 정상 동작을 구분해서 판정한다.