fix: Meta 클릭 ID(fbc) 폴백 및 이벤트 ID 생성 폴백 추가

This commit is contained in:
김성경 2026-07-28 12:00:34 +09:00
parent f36fd5a3cb
commit f19cba7d75

View File

@ -310,7 +310,7 @@ export async function generateVideo(taskId: string, orientation: 'vertical' | 'h
}
// ============================================================
// Meta 전환 추적 (FirstVideoCreated)
// Meta 전환 추적
// ============================================================
// Meta 픽셀이 심는 1st-party 쿠키 값 읽기 (_fbc/_fbp)
@ -319,6 +319,24 @@ function getCookieValue(name: string): string | null {
return match ? decodeURIComponent(match[1]) : null;
}
// URL의 fbclid로 조립해둔 fbc 폴백 값을 보관하는 localStorage 키
// UTM과 분리해 두어야 가입 완료 후 UTM을 정리해도 이후 이벤트에서 계속 쓸 수 있다.
const FBC_STORAGE_KEY = 'castad_fbc';
// Meta 클릭 ID(fbc) 조회
// 픽셀이 심은 _fbc 쿠키를 우선 사용하고, 없으면 랜딩 시 fbclid로 조립해둔 값을 쓴다.
// 애드블로커·쿠키 정리 등으로 쿠키가 유실돼도 광고 클릭 기여를 잃지 않기 위한 폴백이다.
function getFbc(): string | null {
const cookie = getCookieValue('_fbc');
if (cookie) return cookie;
try {
return localStorage.getItem(FBC_STORAGE_KEY);
} catch {
return null;
}
}
// 첫 영상 생성 완료(FirstVideoCreated) 전환 이벤트 발화 API
// 서버가 first_video_created_at null 여부로 계정당 최초 1회를 판정하고,
// fired=true일 때만 브라우저 픽셀(fbq)을 동일 event_id로 발화해 중복제거를 보장한다.
@ -330,7 +348,7 @@ export async function trackFirstVideoCreated(): Promise<void> {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fbc: getCookieValue('_fbc'),
fbc: getFbc(),
fbp: getCookieValue('_fbp'),
event_source_url: window.location.href,
}),
@ -355,19 +373,32 @@ export async function trackFirstVideoCreated(): Promise<void> {
const UTM_STORAGE_KEY = 'castad_utm';
const UTM_PARAM_NAMES = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const;
// 이벤트 중복제거용 UUID 생성
// crypto.randomUUID는 보안 컨텍스트(https/localhost) 전용이라 미지원 환경 폴백 포함
function generateEventId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// fbq 전역 함수 안전 접근
function getFbq(): ((...args: unknown[]) => void) | null {
const fbq = (window as unknown as { fbq?: (...args: unknown[]) => void }).fbq;
return typeof fbq === 'function' ? fbq : null;
}
// 랜딩 진입 URL의 UTM 파라미터를 localStorage에 저장 (first-touch)
// 이미 저장된 값이 있으면 덮어쓰지 않아 최초 유입 경로를 보존한다.
// 랜딩 진입 URL에서 광고 유입 정보(UTM, fbclid)를 localStorage에 보관한다.
export function storeUtmFromUrl(): void {
try {
if (localStorage.getItem(UTM_STORAGE_KEY)) return;
const params = new URLSearchParams(window.location.search);
// UTM: 이미 저장된 값이 있으면 덮어쓰지 않아 최초 유입 경로(first-touch)를 보존
if (!localStorage.getItem(UTM_STORAGE_KEY)) {
const utm: Record<string, string> = {};
for (const name of UTM_PARAM_NAMES) {
const value = params.get(name);
@ -377,6 +408,15 @@ export function storeUtmFromUrl(): void {
if (Object.keys(utm).length > 0) {
localStorage.setItem(UTM_STORAGE_KEY, JSON.stringify(utm));
}
}
// fbclid: 광고 클릭 기여는 최신 클릭이 가져가므로 새 값이 오면 덮어쓴다
// (Meta 픽셀의 _fbc 쿠키도 동일하게 last-touch로 동작)
// 형식: fb.{subdomainIndex}.{creationTime(ms)}.{fbclid}
const fbclid = params.get('fbclid');
if (fbclid) {
localStorage.setItem(FBC_STORAGE_KEY, `fb.1.${Date.now()}.${fbclid}`);
}
} catch (error) {
console.error('[Tracking] storeUtmFromUrl failed:', error);
}
@ -387,10 +427,14 @@ export function trackViewContent(): void {
try {
const fbq = getFbq();
if (fbq) {
fbq('track', 'ViewContent', {}, { eventID: crypto.randomUUID() });
fbq('track', 'ViewContent', {}, { eventID: generateEventId() });
console.log('[Tracking] ViewContent fired');
} else {
console.warn('[Tracking] ViewContent skipped: fbq not loaded');
}
} catch (error) {
console.error('[Tracking] ViewContent failed:', error);
const detail = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : String(error);
console.error('[Tracking] ViewContent failed:', detail);
}
}
@ -412,7 +456,7 @@ export async function trackCompleteRegistration(): Promise<void> {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fbc: getCookieValue('_fbc'),
fbc: getFbc(),
fbp: getCookieValue('_fbp'),
event_source_url: window.location.href,
utm_source: utm.utm_source ?? null,