/** * CDP Edge — TikTok Events API v1.3 * Envia eventos server-side para a TikTok Events API. */ import { sha256, normalizePhone } from '../utils.js'; import { logApiFailure } from '../db.js'; import { Env, TrackPayload } from '../../types.js'; import { ExecutionContext } from '@cloudflare/workers-types'; export async function sendTikTokApi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise { if (env.DISABLE_EXTERNAL_DISPATCH === '1') return { skipped: 'external dispatch disabled' }; if (!env.TIKTOK_ACCESS_TOKEN) return { skipped: 'TIKTOK_ACCESS_TOKEN not set' }; const pixelId = env.TIKTOK_PIXEL_ID; if (!pixelId || pixelId === 'SEU_TIKTOK_PIXEL_ID') return { skipped: 'TIKTOK_PIXEL_ID not configured' }; const { email, phone, firstName, lastName, fbp, fbc, ttp, ttclid, userId, eventId, pageUrl, value, currency, contentIds, contentName, contentType, } = payload; const phoneNorm = normalizePhone(phone); const user: Record = { ...(email && { email: await sha256(email) || '' }), ...(phoneNorm && { phone_number: await sha256(phoneNorm) || '' }), ...(userId && { external_id: await sha256(String(userId)) || '' }), ...(ttp && { ttp }), ...(ttclid && { ttclid }), }; const properties: Record = { ...(value !== undefined && { value: parseFloat(String(value)) }), ...(currency && { currency: String(currency).toUpperCase() }), ...(contentIds && contentIds && contentIds.length > 0 && { contents: contentIds.map(id => ({ content_id: String(id), content_name: contentName || '', content_type: contentType || 'product', quantity: 1, price: value ? parseFloat(String(value)) : 0, })), }), }; const event: Record = { event: eventName, event_time: Math.floor(Date.now() / 1000), event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, user, page: { url: pageUrl || `https://${env.SITE_DOMAIN}`, referrer: request?.headers.get('Referer') || '', }, ...(Object.keys(properties).length > 0 && { properties }), context: { ip: request?.headers.get('CF-Connecting-IP') || '', user_agent: request?.headers.get('User-Agent') || '', }, }; const body = { event_source: 'web', event_source_id: pixelId, data: [event], }; // Endpoint canônico: sempre /v1.3/event/track/ const endpoint = 'https://business-api.tiktok.com/open_api/v1.3/event/track/'; try { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Access-Token': env.TIKTOK_ACCESS_TOKEN, }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok || (data as any).code !== 0) { console.error('TikTok Events API error:', res.status, (data as any).message || (data as any).code || 'unknown'); if (env.DB && ctx) { ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, String((data as any).code || res.status), (data as any).message || 'TikTok API error', event.event_id, JSON.stringify(body))); } } return data; } catch (err: any) { console.error('TikTok Events API fetch failed:', err?.message || String(err)); if (env.DB && ctx) { ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body))); } if (env.RETRY_QUEUE) { const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'tiktok' }); if (ctx) ctx.waitUntil(send); else send.catch(() => {}); } return { error: err?.message || String(err) }; } }