# Knowledge Base: CDP Edge (Quantum Tier)

Esta é a fonte técnica oficial para o CDP Edge. Toda implementação deve seguir os padrões de infraestrutura Cloudflare Native (Workers + D1) para garantir máxima atribuição e performance.

---

## 🏗️ 1. ARQUITETURA DE DADOS (CLOUDFLARE NATIVE)

### Camadas do Sistema:
1.  **Browser (Coleta)**: O SDK `cdpTrack.js` captura eventos de interação, dados de identidade e **micro-comportamentos**.
2.  **Worker (Processamento)**: O Cloudflare Worker recebe os dados, normaliza, executa o hashing WebCrypto e persiste no banco D1.
3.  **D1 Database (Persistência)**: Fonte de verdade para o Identity Graph e log de eventos completo (Low-level).
4.  **APIs (Despacho)**: Envio seletivo e assíncrono para Meta, TikTok e GA4 via `ctx.waitUntil`.

### Estratégia: D1 vs. Plataformas (EMQ Optimization)
Para manter o Meta Pixel limpo e focado em conversão, aplicamos a regra de **Filtragem de Intent**:

*   **D1 (Tudo)**: Salva 100% dos eventos (cliques x/y, rage clicks, scrolls, heartbeats). É sua ferramenta de auditoria e BI.
*   **Plataformas (Meta/Google)**: Recebe apenas eventos de **Alta Intenção** (Milestones):
    *   `Retention_Pulse` (Ex: usuário ativo por +60s).
    *   `Rage_Click` (Exclusão técnica).
    *   `VSL_25/50/75/100` (Funis de vídeo).
    *   Eventos Standard (`Lead`, `Purchase`, `AddToCart`).

---

## 🛠️ 2. PADRÕES TÉCNICOS

### 2.1 Identity Graph (Lead Lock)
A identidade do usuário é o pilar da atribuição. Ela vincula identificadores anônimos a dados reais (PII) de forma segura.
- **Campos**: `email_hash`, `phone_hash`, `fbp`, `fbc`, `ttp`, `ttclid`, `ip`, `ua`.
- **Lógica**: Quando um Webhook de venda chega, o sistema busca no D1 pelo e-mail/telefone para recuperar os cookies de browser originais, garantindo Match Quality máximo.

### 2.2 Deduplicação (Event ID)
- Todo evento disparado no browser gera um `event_id` único.
- Esse mesmo ID deve ser enviado na chamada de servidor (CAPI) para que as plataformas ignorem duplicatas.

---

## 🏗️ 3. HUMAN-BEHAVIOR ENGINE (MICRO-EVENTOS)

Protocolos de captura para análise de CRO e saúde técnica.

### 3.1 Rage Click Detector
Detecta quando o usuário clica repetidamente (>3 cliques em 500ms) em uma área não interativa.
*   **Ação**: Disparar `rage_click`.
*   **Uso**: Identificar bugs de UI ou frustração do usuário.

### 3.2 Visibility Heartbeat (Tab Focus)
Detecta se o usuário mudou de aba (`document.visibilityState === 'hidden'`).
*   **Ação**: Disparar `tab_visibility_change` com status `hidden` ou `visible`.
*   **Uso**: Medir retenção real em VSLs onde o áudio continua mas o vídeo não é visto.

### 3.3 Click Heatmap (D1 Only)
Captura as coordenadas relativas do clique no documento.
*   **Schema D1**: `x_pos` (float), `y_pos` (float), `element_id` (string), `element_class` (string).
*   **Uso**: Reconstrução de mapas de calor sem ferramentas externas.

### 3.4 Retention Pulse
Disparado em intervalos fixos (30s, 60s, 120s) para medir o tempo de atenção ativa.
*   **Ação**: Disparar `pulse_heartbeat` com o atributo `duration`.
*   **Uso**: Treinar a IA da Meta com usuários de "Alto Tempo de Atenção".

---

## 🛠️ 4. GERADOR DE TRACKING (SDK UNIFICADO)

**Gerado uma vez. Nunca editar.** Despacha cada evento para todas as plataformas ativas automaticamente.

```js
// src/tracking/tracking.js
// ============================================================
// CDPEDGE — Utilitário multi-plataforma unificado
// Suporta: Meta · GA4 · Google Ads · TikTok · Pinterest · Bing · Reddit
// NÃO edite — edite apenas tracking.config.js
// ============================================================
import CONFIG from './tracking.config.js';

// ── Guards — segurança em SSR e SDK não carregado ──
const isBrowser = typeof window !== 'undefined';
const has = (fn) => isBrowser && typeof window[fn] === 'function';

// ── Captura de parâmetros de URL no carregamento da página ──
// Cada plataforma injeta seu próprio Click ID na URL quando o usuário clica em um anúncio.
// Guardamos todos em memória para enviar ao servidor e garantir atribuição correta.
const _urlParams = isBrowser ? new URLSearchParams(window.location.search) : new URLSearchParams();

// Meta
const _ctwaClid = _urlParams.get('ctwa_clid') || '';  // Click-to-WhatsApp Click ID
const _fbclid   = _urlParams.get('fbclid')    || '';  // Meta Ads click ID → gera cookie _fbc

// Google Ads (três variantes por tipo de match/privacy)
const _gclid   = _urlParams.get('gclid')   || '';    // Google Ads standard click ID → gera cookie _gcl_aw
const _wbraid  = _urlParams.get('wbraid')  || '';    // Google Ads (iOS, web-to-app, privacy preserving)
const _gbraid  = _urlParams.get('gbraid')  || '';    // Google Ads (app campaigns, privacy preserving)

// TikTok
const _ttclid  = _urlParams.get('ttclid')  || '';    // TikTok Ads click ID → complementa cookie _ttp

// UTMs — rastreamento interno de origem de tráfego (independente da atribuição das plataformas)
const _utms = {
  utm_source:   _urlParams.get('utm_source')   || '',
  utm_medium:   _urlParams.get('utm_medium')   || '',
  utm_campaign: _urlParams.get('utm_campaign') || '',
  utm_content:  _urlParams.get('utm_content')  || '',
  utm_term:     _urlParams.get('utm_term')     || '',
};

// ── User ID persistente (first-party cookie) ──────────────────────────────────
// Identifica o mesmo usuário entre sessões diferentes, mesmo sem login.
// Resolve o problema de cookies de terceiros bloqueados por ad-blockers.
const _getUserId = () => {
  if (!isBrowser) return '';
  const KEY = '_cdp_uid';
  let uid = document.cookie.match(new RegExp(`${KEY}=([^;]+)`))?.[1];
  if (!uid) {
    uid = `${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
    // Cookie first-party de 365 dias — não bloqueado por navegadores
    document.cookie = `${KEY}=${uid}; max-age=${60*60*24*365}; path=/; SameSite=Lax`;
  }
  return uid;
};
const _userId = isBrowser ? _getUserId() : '';

/** getUTMs() — retorna os UTMs capturados na chegada do usuário
 *  Útil para: salvar no CRM, incluir no payload do servidor, rastreamento interno.
 *  Atribuição própria independente da janela das plataformas (último clique registrado). */
export const getUTMs = () => ({ ..._utms });

/** getUserId() — retorna o User ID persistente gerado como cookie first-party.
 *  Identifica o mesmo usuário entre sessões. Não é bloqueado por ad-blockers.
 *  Identificador de usuário persistente nativo. */
export const getUserId = () => _userId;

/**
 * passCheckoutParams(options?) — adiciona UTMs + User ID nos links de checkout externo.
 *
 * Problema: quando o usuário clica em um link de Hotmart, Kiwify, Eduzz etc., os UTMs e
 * o user ID são perdidos — a plataforma não sabe de onde veio o comprador.
 *
 * Solução: interceptar os links de checkout e adicionar os parâmetros de rastreamento
 * na URL antes do clique, para que a plataforma receba e registre a origem.
 *
 * Plataformas suportadas e seus parâmetros:
 *   hotmart  → xcod (user ID) + sck (UTMs pipe-separados: src|med|camp|con|term)
 *   kiwify   → src (utm_source)
 *   eduzz    → src (utm_source)
 *   monetizze → src (utm_source)
 *   cartpanda → utm_* passthrough direto
 *   ticto    → utm_* + user_id (_cdp_uid) — capturado em wh.tracking e wh.url_params
 *   custom   → qualquer domínio via opção `domains`
 *
 * Uso:
 *   passCheckoutParams()                              // detecta automaticamente
 *   passCheckoutParams({ platforms: ['hotmart'] })   // só Hotmart
 *   passCheckoutParams({ domains: ['meusite.com/checkout'] }) // domínio customizado
 */
export const passCheckoutParams = (options = {}) => {
  if (!isBrowser) return;

  const {
    platforms = ['hotmart', 'kiwify', 'eduzz', 'monetizze', 'cartpanda', 'ticto'],
    domains   = [],   // domínios extras além das plataformas padrão
    extra     = {},   // parâmetros extras a adicionar em todos os links
  } = options;

  const utms   = getUTMs();
  const userId = _userId;

  // Mapa de domínios por plataforma
  const PLATFORM_DOMAINS = {
    hotmart:    ['hotmart.com', 'pay.hotmart.com', 'payment.hotmart.com'],
    kiwify:     ['kiwify.com.br', 'checkout.kiwify.com.br'],
    eduzz:      ['eduzz.com', 'sun.eduzz.com'],
    monetizze:  ['monetizze.com.br'],
    cartpanda:  ['cartpanda.com', 'pay.cartpanda.com'],
    ticto:      ['ticto.app', 'pay.ticto.app', 'checkout.ticto.app'],
  };

  // Constrói o `sck` do Hotmart: utm_source|utm_medium|utm_campaign|utm_content|utm_term
  const buildSck = () =>
    [utms.utm_source, utms.utm_medium, utms.utm_campaign, utms.utm_content, utms.utm_term]
      .map(v => v || 'direto')
      .join('|');

  // Parâmetros por plataforma
  const getParamsForUrl = (href) => {
    const params = { ...extra };
    const url    = href.toLowerCase();

    const isMatch = (list) => list.some(d => url.includes(d));

    if (platforms.includes('hotmart') && isMatch(PLATFORM_DOMAINS.hotmart)) {
      if (userId)              params.xcod = userId;
      if (utms.utm_source)     params.sck  = buildSck();
    } else if (platforms.includes('kiwify') && isMatch(PLATFORM_DOMAINS.kiwify)) {
      if (utms.utm_source)     params.src  = utms.utm_source;
      if (utms.utm_medium)     params.utm_medium   = utms.utm_medium;
      if (utms.utm_campaign)   params.utm_campaign = utms.utm_campaign;
    } else if (
      (platforms.includes('eduzz')     && isMatch(PLATFORM_DOMAINS.eduzz))    ||
      (platforms.includes('monetizze') && isMatch(PLATFORM_DOMAINS.monetizze))
    ) {
      if (utms.utm_source)     params.src  = utms.utm_source;
    } else if (platforms.includes('cartpanda') && isMatch(PLATFORM_DOMAINS.cartpanda)) {
      // CartPanda recebe utm_* diretamente
      Object.entries(utms).forEach(([k, v]) => { if (v) params[k] = v; });
    } else if (platforms.includes('ticto') && isMatch(PLATFORM_DOMAINS.ticto)) {
      Object.entries(utms).forEach(([k, v]) => { if (v) params[k] = v; });
      if (userId) params.user_id = userId;
    } else {
      // Domínios customizados: repassa utm_* e userId
      const allDomains = Object.values(PLATFORM_DOMAINS).flat().concat(domains);
      if (!isMatch(allDomains)) return null;  // não é checkout conhecido
      Object.entries(utms).forEach(([k, v]) => { if (v) params[k] = v; });
      if (userId) params.user_id = userId;
    }

    return Object.keys(params).length ? params : null;
  };

  const applyParams = (link) => {
    if (!link.href || link.href.startsWith('javascript')) return;
    const p = getParamsForUrl(link.href);
    if (!p) return;

    try {
      const url    = new URL(link.href);
      Object.entries(p).forEach(([k, v]) => url.searchParams.set(k, v));
      link.href = url.toString();
    } catch { /* URL inválida — ignora */ }
  };

  // Aplica em todos os links existentes
  document.querySelectorAll('a[href]').forEach(applyParams);

  // Aplica em links adicionados dinamicamente (ex: botões de checkout lazy loaded)
  new MutationObserver((mutations) => {
    mutations.forEach(m => m.addedNodes.forEach(node => {
      if (node.nodeType !== 1) return;
      if (node.tagName === 'A') applyParams(node);
      node.querySelectorAll?.('a[href]').forEach(applyParams);
    }));
  }).observe(document.body, { childList: true, subtree: true });
};

// ── Utilitários de formulário ─────────────────────────────────────────────────
// Normalizam dados ANTES de enviar às plataformas, alinhados com as variáveis


/** normalizePhone(tel) — normaliza telefone para o formato exigido pelas plataformas.
 *  Remove tudo que não é número. Adiciona DDI Brasil 55 se necessário.
 *  Meta exige somente dígitos com DDI — ex: '5511999998888'
 *  Retorna undefined se vazio (evita enviar campo em branco às APIs). */
export const normalizePhone = (tel = '') => {
  let t = String(tel).replace(/\D/g, '');
  if (!t) return undefined;
  if (!t.startsWith('55') || t.length <= 11) t = '55' + t;
  return t;
};

/** splitName(fullName) — divide nome completo em firstName e lastName.
 *  firstName = todos os nomes menos o último (alinha com campo Meta fn).
 *  lastName  = somente o último nome (alinha com campo Meta ln).
 *  Ex: 'João da Silva' → { firstName: 'João da', lastName: 'Silva' }
 *  Ex: 'João' → { firstName: 'João', lastName: undefined } */
export const splitName = (fullName = '') => {
  const parts = String(fullName).trim().split(/\s+/).filter(Boolean);
  if (!parts.length) return { firstName: undefined, lastName: undefined };
  if (parts.length === 1) return { firstName: parts[0], lastName: undefined };
  return { firstName: parts.slice(0, -1).join(' '), lastName: parts.at(-1) };
};

/** isValidEmail(email) — valida formato de email antes de enviar.
 *  Evita poluir os dados das plataformas com emails inválidos.
 *  Validação de Email nativa do CDP Edge.
 *  Retorna true se válido, false caso contrário. */
export const isValidEmail = (email = '') =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email).toLowerCase().trim());

/** readField(selector) — lê valor de campo de formulário por id, name ou seletor CSS.
 *  ex: readField('email') → busca id="email" ou name="email"
 *  ex: readField('#meu-email') ou readField('input[name="telefone"]') */
export const readField = (selector = '') => {
  if (!isBrowser) return '';
  const el = /^[#.[]/.test(selector)
    ? document.querySelector(selector)
    : document.getElementById(selector) || document.querySelector(`[name="${selector}"]`);
  return el?.value?.trim() || '';
};

/** normalizeCity(city) — normaliza cidade para o campo `ct` do Meta Advanced Matching.
 *  Regra Meta: lowercase, sem acentos, sem espaços, sem caracteres especiais.
 *  ex: 'São Paulo' → 'saopaulo' | 'Belo Horizonte' → 'belohorizonte'
 *  DEVE ser aplicada ANTES do SHA256 no lado servidor (CAPI). */
export const normalizeCity = (city = '') => {
  if (!city) return undefined;
  return String(city)
    .normalize('NFD')                    // decompõe acentos (e.g. ã → a + ~)
    .replace(/[\u0300-\u036f]/g, '')     // remove diacríticos
    .toLowerCase()
    .replace(/[^a-z0-9]/g, '');         // remove tudo que não é letra ou número
};

// ── Utilitários de DataLayer / Items ─────────────────────────────────────────
// Transformam qualquer estrutura de produto para o formato GA4 items (a base de tudo).
// O GA4 items é a "língua universal" — de lá convertemos para Meta, TikTok, etc.

/**
 * fixItems(malFormatted) — converte items MAL FORMATADOS para o padrão GA4.
 * Recebe objeto ou array com qualquer estrutura de campo e mapeia para GA4.
 * Uso: quando o dataLayer do site/plataforma chega com nomes diferentes dos esperados.
 *
 * Parâmetro fieldMap: { ga4Campo: 'nomeCampoOrigem' }
 * Ex: fixItems(dlItems, { item_id:'id', item_name:'nome', price:'valor', quantity:'qtd' })
 *
 * Se não passar fieldMap, tenta detecção automática de campos comuns.
 */
export const fixItems = (malFormatted, fieldMap = {}) => {
  if (!malFormatted) return [];
  const arr = Array.isArray(malFormatted) ? malFormatted : [malFormatted];
  const fm = {
    item_id:   fieldMap.item_id   || 'id',
    item_name: fieldMap.item_name || 'name' ,
    price:     fieldMap.price     || 'price',
    quantity:  fieldMap.quantity  || 'quantity',
    currency:  fieldMap.currency  || 'currency',
    item_brand:    fieldMap.item_brand    || 'brand',
    item_category: fieldMap.item_category || 'category',
    item_variant:  fieldMap.item_variant  || 'variant',
  };
  return arr.map((item, index) => ({
    item_id:       item[fm.item_id]   || item.sku || item.product_id || '',
    item_name:     item[fm.item_name] || item.title || item.nome || item.produto || '',
    price:         parseFloat(String(item[fm.price] || 0).replace(',', '.')),
    quantity:      parseInt(item[fm.quantity] || 1, 10),
    index,
    ...(item[fm.currency]     && { currency:      item[fm.currency] }),
    ...(item[fm.item_brand]   && { item_brand:    item[fm.item_brand] }),
    ...(item[fm.item_category]&& { item_category: item[fm.item_category] }),
    ...(item[fm.item_variant] && { item_variant:  item[fm.item_variant] }),
    // campos opcionais GA4 — passados direto se presentes
    ...(item.item_category2   && { item_category2: item.item_category2 }),
    ...(item.item_category3   && { item_category3: item.item_category3 }),
    ...(item.affiliation      && { affiliation:    item.affiliation }),
    ...(item.coupon           && { coupon:         item.coupon }),
    ...(item.discount         && { discount:       parseFloat(item.discount) }),
    ...(item.item_list_id     && { item_list_id:   item.item_list_id }),
    ...(item.item_list_name   && { item_list_name: item.item_list_name }),
    ...(item.item_variant     && { item_variant:   item.item_variant }),
    ...(item.location_id      && { location_id:    item.location_id }),
    ...(item.promotion_id     && { promotion_id:   item.promotion_id }),
    ...(item.promotion_name   && { promotion_name: item.promotion_name }),
  }));
};

/**
 * buildItems(params) — monta array GA4 a partir de parâmetros SOLTOS.
 * Uso: quando o produto chega em campos separados (product_id, product_name, etc.)
 * em vez de um array items.
 *
 * Ex: buildItems({ item_id: 'SKU123', item_name: 'Produto', price: 99.9 })
 *   → [{ item_id: 'SKU123', item_name: 'Produto', price: 99.9, quantity: 1 }]
 */
export const buildItems = (params = {}) => [{
  item_id:       params.item_id || params.product_id || params.id || '',
  item_name:     params.item_name || params.product_name || params.name || params.nome || '',
  price:         parseFloat(params.price || params.valor || 0),
  quantity:      parseInt(params.quantity || params.quantidade || 1, 10),
  currency:      params.currency || 'BRL',
  ...(params.item_brand    && { item_brand:    params.item_brand }),
  ...(params.item_category && { item_category: params.item_category }),
  ...(params.item_variant  && { item_variant:  params.item_variant }),
  ...(params.item_category2&& { item_category2:params.item_category2 }),
  ...(params.discount      && { discount:      parseFloat(params.discount) }),
  ...(params.coupon        && { coupon:        params.coupon }),
  ...(params.affiliation   && { affiliation:   params.affiliation }),
}];

/**
 * ga4ItemsToMeta(items) — converte GA4 items para formato de content_ids + contents do Meta.
 * Usado internamente pelo trackPurchase / trackAddToCart quando recebem _ga4Items.
 * Converte dados para o formato Schema Standard da Meta.
 */
export const ga4ItemsToMeta = (items = []) => ({
  content_ids: items.map(i => String(i.item_id)),
  contents:    items.map(i => ({
    id:         String(i.item_id),
    quantity:   i.quantity   || 1,
    item_price: i.price      || 0,
    title:      i.item_name  || undefined,
    category:   i.item_category || undefined,
    brand:      i.item_brand || undefined,
  })),
  num_items:   items.reduce((s, i) => s + (i.quantity || 1), 0),
  value:       items.reduce((s, i) => s + (i.price || 0) * (i.quantity || 1), 0),
});

/**
 * ga4ItemsToTikTok(items) — converte GA4 items para formato de contents do TikTok Pixel.
 * Converte dados para o formato Schema Standard do TikTok.
 */
export const ga4ItemsToTikTok = (items = []) => ({
  contents: items.map(i => ({
    content_id:       String(i.item_id),
    content_name:     i.item_name     || undefined,
    content_category: i.item_category || undefined,
    brand:            i.item_brand    || undefined,
    price:            i.price         || 0,
    quantity:         i.quantity      || 1,
  })),
  value: items.reduce((s, i) => s + (i.price || 0) * (i.quantity || 1), 0),
});

// ── Dispatchers por plataforma ──
const meta     = (type, name, p = {}) => has('fbq')    && window.fbq(type, name, p);
const tiktok   = (name, p = {})       => has('ttq')    && window.ttq.track(name, p);
const pinterest = (name, p = {})      => has('pintrk') && window.pintrk('track', name, p);
const bing     = (name, p = {})       => isBrowser && window.uetq?.push({ ea: name, ...p });
const reddit   = (name, p = {})       => has('rdt')    && window.rdt('track', name, p);
const gtag     = (...a)               => has('gtag') && window.gtag(...a);

// ── Verificação de credencial ──
const active = (key) => !!CONFIG[key];

// ── Dispatcher GA4 ──
const ga4Event = (name, params = {}) => gtag('event', name, params);
const ga4Ecommerce = (name, data) => gtag('event', name, { ecommerce: data });

// ── Conversão Google Ads (com Enhanced Conversions / Conversões Otimizadas) ──
const gadsConversion = (label, value = 0, currency = 'BRL', txId = '', email = '', phone = '') => {
  if (!label || !active('googleAdsId')) return;
  gtag('event', 'conversion', {
    send_to: `${CONFIG.googleAdsId}/${label}`,
    value, currency,
    ...(txId && { transaction_id: txId }),
    // Enhanced Conversions (Conversões Otimizadas) — atribuição a nível de usuário
    ...(email     && { email_address: email }),
    ...(phone     && { phone_number:  phone }),
  });
};

// ============================================================
// API PÚBLICA
// ============================================================

/** PageView — chame em cada página via useEffect(()=>{}, []) */
export const trackPageView = (title = document.title, url = window.location.pathname) => {
  if (active('metaPixelId'))    meta('track', 'PageView');
  if (active('tiktokPixelId'))  tiktok('PageView');                // ttq.page() automático
  if (active('pinterestTagId')) pinterest('pagevisit');
  if (active('redditPixelId'))  reddit('PageVisit');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'pageLoad' }); // Bing
  ga4Event('page_view', { page_title: title, page_location: url });
};

/** ViewContent — ao carregar a página do produto/imóvel/serviço (NO useEffect, não no scroll) */
export const trackViewContent = (params = {}) => {
  const { _ga4Items = [], _tiktok = {}, ...metaParams } = params;

  if (active('metaPixelId'))    meta('track', 'ViewContent', metaParams);
  if (active('tiktokPixelId'))  tiktok('ViewContent', {
    content_id: (_tiktok.content_id || metaParams.content_ids?.[0] || ''),
    content_name: metaParams.content_name || '',
    content_type: metaParams.content_type || 'product',
    value: metaParams.value || 0,
    currency: metaParams.currency || 'BRL',
    ..._tiktok
  });
  if (active('pinterestTagId')) pinterest('viewcategory', {
    product_id: metaParams.content_ids?.[0] || '',
    product_name: metaParams.content_name || '',
    value: metaParams.value || 0,
    currency: metaParams.currency || 'BRL',
  });
  if (active('redditPixelId'))  reddit('ViewContent', { value: metaParams.value, currency: metaParams.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_content' });

  ga4Ecommerce('view_item', {
    currency: metaParams.currency || 'BRL',
    value: metaParams.value || 0,
    items: _ga4Items
  });
};

// ── Geolocalização nativa (cidade do lead via OpenStreetMap — gratuito) ──
const getCidade = () => new Promise((resolve) => {
  if (!isBrowser || !navigator.geolocation) return resolve(null);
  navigator.geolocation.getCurrentPosition(
    async ({ coords: { latitude, longitude } }) => {
      try {
        const r = await fetch(
          `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json`,
          { headers: { 'Accept-Language': 'pt-BR' } }
        );
        const d = await r.json();
        resolve(d.address?.city || d.address?.town || d.address?.municipality || null);
      } catch { resolve(null); }
    },
    () => resolve(null),          // usuário negou permissão → ignora silenciosamente
    { timeout: 5000 }
  );
});

/** Lead — envio de formulário (CONVERSÃO PRINCIPAL) — captura cidade automaticamente */
export const trackLead = async (params = {}) => {
  const eventId = genId();                                   // ID único — deduplicação browser ↔ servidor
  const cidade  = await getCidade();                         // tenta capturar cidade
  const base    = { value: 0, currency: 'BRL', ...params, ...(cidade && { city: cidade }) };

  // Meta: eventID no 3º argumento → deduplicação com CAPI
  if (active('metaPixelId'))    meta('track', 'Lead', base, { eventID: eventId });
  if (active('tiktokPixelId')) {
    // ttq.identify(): Advanced Matching — vincula eventos ao usuário (cross-session)
    // Chamar ANTES do track para que o evento já carregue o match key
    if ((base.email || base.phone) && has('ttq')) {
      window.ttq.identify({
        email:        base.email?.toLowerCase().trim() || '',
        phone_number: base.phone ? `+${String(base.phone).replace(/\D/g, '')}` : '',
        external_id:  _userId || '',
      });
    }
    tiktok('Lead', { content_name: base.content_name || '', value: base.value, currency: base.currency });
  }
  if (active('pinterestTagId')) pinterest('lead', { lead_type: base.content_category || 'formulario', value: base.value, currency: base.currency });
  if (active('redditPixelId'))  reddit('Lead', { value: base.value, currency: base.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'submit_form', ev: base.value || 0, ec: base.currency || 'BRL' });

  // event_id incluído para deduplicação browser ↔ servidor
  ga4Event('generate_lead', { method: 'formulario', currency: base.currency, value: base.value, event_id: eventId, ...(cidade && { city: cidade }) });
  gadsConversion(CONFIG.googleAdsConversions?.lead, base.value, base.currency, '', base.email, base.phone);
};

/** Contact — clique em WhatsApp / telefone — captura cidade e ctwa_clid automaticamente */
export const trackContact = async (method = 'whatsapp') => {
  const eventId = genId();
  const cidade  = await getCidade();
  const extra   = cidade ? { city: cidade } : {};

  // Meta: passa eventID para deduplicação com CAPI
  if (active('metaPixelId'))    has('fbq') && window.fbq('track', 'Contact', extra, { eventID: eventId });
  if (active('tiktokPixelId'))  tiktok('Contact', extra);
  if (active('pinterestTagId')) pinterest('lead', { lead_type: method });
  if (active('redditPixelId'))  reddit('Lead', extra);
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'contact', el: method });

  // event_id incluído para deduplicação Meta
  ga4Event('generate_lead', { method, currency: 'BRL', value: 0, event_id: eventId, ...extra });
  gadsConversion(CONFIG.googleAdsConversions?.whatsapp);

  // Server-side: envia ctwa_clid se veio de anúncio Click-to-WhatsApp
  sendServerEvent('Contact', eventId, {
    city:     cidade || '',
    method,
    // ctwaClid é incluído automaticamente via getCookies() dentro de sendServerEvent
  });
};

/** Schedule — agendamento */
export const trackSchedule = () => {
  if (active('metaPixelId'))    meta('track', 'Schedule');
  if (active('tiktokPixelId'))  tiktok('Schedule');
  if (active('pinterestTagId')) pinterest('lead', { lead_type: 'agendamento' });
  if (active('redditPixelId'))  reddit('Lead');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'schedule' });

  ga4Event('generate_lead', { method: 'agendamento', currency: 'BRL', value: 0 });
  gadsConversion(CONFIG.googleAdsConversions?.schedule);
};

/** InitiateCheckout — clique em "Comprar" */
export const trackInitiateCheckout = (params = {}) => {
  const { _ga4Items = [], ...p } = params;
  const base = { currency: 'BRL', value: 0, ...p };

  if (active('metaPixelId'))    meta('track', 'InitiateCheckout', base);
  if (active('tiktokPixelId'))  tiktok('InitiateCheckout', { value: base.value, currency: base.currency, content_id: base.content_ids?.[0] });
  if (active('pinterestTagId')) pinterest('checkout', { value: base.value, currency: base.currency, order_quantity: 1 });
  if (active('redditPixelId'))  reddit('AddToCart', { value: base.value, currency: base.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'initiate_checkout', ev: base.value, ec: base.currency });

  ga4Ecommerce('begin_checkout', { currency: base.currency, value: base.value, items: _ga4Items });
};

/** AddToCart — adicionar ao carrinho */
export const trackAddToCart = (params = {}) => {
  const { _ga4Items = [], ...p } = params;
  const base  = { currency: 'BRL', value: 0, ...p };
  // Converte GA4 items → formato Meta/TikTok automaticamente (Universal Conversion)
  const metaP = _ga4Items.length ? { ...base, ...ga4ItemsToMeta(_ga4Items) } : base;
  const ttkP  = _ga4Items.length ? ga4ItemsToTikTok(_ga4Items) : { value: base.value, currency: base.currency };

  if (active('metaPixelId'))    meta('track', 'AddToCart', metaP);
  if (active('tiktokPixelId'))  tiktok('AddToCart', { ...ttkP, currency: base.currency, content_id: metaP.content_ids?.[0] });
  if (active('pinterestTagId')) pinterest('addtocart', { value: metaP.value, currency: base.currency, product_id: metaP.content_ids?.[0] });
  if (active('redditPixelId'))  reddit('AddToCart', { value: metaP.value, currency: base.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_to_cart', ev: metaP.value, ec: base.currency });

  ga4Ecommerce('add_to_cart', { currency: base.currency, value: metaP.value, items: _ga4Items });
};

/** Purchase — compra concluída (value + currency obrigatórios) */
export const trackPurchase = (params = {}) => {
  const { _ga4Items = [], ...p } = params;
  if (!p.value || !p.currency) console.warn('[CDP Edge] trackPurchase requer value e currency');
  const txId  = p.order_id || `T_${Date.now()}`;
  // Converte GA4 items → formato Meta/TikTok automaticamente (Universal Conversion)
  const metaP = _ga4Items.length ? { ...p, ...ga4ItemsToMeta(_ga4Items) } : p;
  const ttkP  = _ga4Items.length ? ga4ItemsToTikTok(_ga4Items) : { value: p.value };

  if (active('metaPixelId'))    meta('track', 'Purchase', { currency: p.currency || 'BRL', ...metaP });
  if (active('tiktokPixelId')) {
    // identify() antes do track: Advanced Matching vincula compra ao usuário
    if ((p.email || p.phone) && has('ttq')) {
      window.ttq.identify({
        email:        p.email?.toLowerCase().trim() || '',
        phone_number: p.phone ? `+${String(p.phone).replace(/\D/g, '')}` : '',
        external_id:  _userId || '',
      });
    }
    tiktok('CompletePayment', { ...ttkP, currency: p.currency || 'BRL', order_id: txId });
  }
  if (active('pinterestTagId')) pinterest('checkout', { value: metaP.value, currency: p.currency || 'BRL', order_id: txId, order_quantity: metaP.num_items || 1 });
  if (active('redditPixelId'))  reddit('Purchase', { value: metaP.value, currency: p.currency || 'BRL' });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'purchase', ev: p.value, ec: p.currency || 'BRL', gc: txId });

  // event_id = txId incluído para deduplicação Meta
  ga4Ecommerce('purchase', { transaction_id: txId, event_id: txId, currency: p.currency || 'BRL', value: p.value, items: _ga4Items });
  gadsConversion(CONFIG.googleAdsConversions?.purchase, p.value, p.currency || 'BRL', txId, p.email, p.phone);
};

/** CompleteRegistration — cadastro concluído */
export const trackSignUp = (method = 'formulario') => {
  if (active('metaPixelId'))    meta('track', 'CompleteRegistration', { status: 'completed' });
  if (active('tiktokPixelId'))  tiktok('CompleteRegistration');
  if (active('pinterestTagId')) pinterest('signup', { lead_type: method });
  if (active('redditPixelId'))  reddit('SignUp');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'sign_up' });

  ga4Event('sign_up', { method });
  gadsConversion(CONFIG.googleAdsConversions?.signup);
};

// ============================================================
// EVENTOS COMPLETOS — E-COMMERCE E FUNIL AVANÇADO
// ============================================================

/** Search — busca no site */
export const trackSearch = (query = '') => {
  if (active('metaPixelId'))    meta('track', 'Search', { search_string: query });
  if (active('tiktokPixelId'))  tiktok('Search', { query });
  if (active('redditPixelId'))  reddit('Search');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'search', el: query });
  ga4Event('search', { search_term: query });
};

/** AddToWishlist — adicionar à lista de desejos / favoritos */
export const trackAddToWishlist = (params = {}) => {
  const { _ga4Items = [], ...p } = params;
  const base = { currency: 'BRL', value: 0, ...p };

  if (active('metaPixelId'))    meta('track', 'AddToWishlist', base);
  if (active('tiktokPixelId'))  tiktok('AddToWishlist', { content_id: base.content_ids?.[0], value: base.value, currency: base.currency });
  if (active('redditPixelId'))  reddit('AddToWishlist');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_to_wishlist', ev: base.value, ec: base.currency });
  ga4Ecommerce('add_to_wishlist', { currency: base.currency, value: base.value, items: _ga4Items });
};

/** RemoveFromCart — remover item do carrinho */
export const trackRemoveFromCart = (params = {}) => {
  const { _ga4Items = [], ...p } = params;
  const base = { currency: 'BRL', value: 0, ...p };

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'remove_from_cart', ev: base.value, ec: base.currency });
  ga4Ecommerce('remove_from_cart', { currency: base.currency, value: base.value, items: _ga4Items });
  // Meta e TikTok não têm RemoveFromCart padrão — usar trackCustom se necessário
};

/** ViewCart — visualizar o carrinho */
export const trackViewCart = (params = {}) => {
  const { _ga4Items = [], ...p } = params;
  const base = { currency: 'BRL', value: 0, ...p };

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_cart' });
  ga4Ecommerce('view_cart', { currency: base.currency, value: base.value, items: _ga4Items });
};

/** ViewItemList — listagem de produtos (categoria, busca, vitrine) */
export const trackViewItemList = (params = {}) => {
  // aceita item_list_id/item_list_name (GA4 spec) ou list_id/list_name (compat)
  const {
    _ga4Items = [], items = _ga4Items,
    item_list_id = params.list_id || '',
    item_list_name = params.list_name || '',
  } = params;

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_item_list' });
  ga4Ecommerce('view_item_list', { item_list_name, item_list_id, items });
};

/** SelectItem — clicou em produto na lista */
export const trackSelectItem = (params = {}) => {
  const {
    _ga4Items = [], items = _ga4Items,
    item_list_id = params.list_id || '',
    item_list_name = params.list_name || '',
  } = params;

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'select_item' });
  ga4Ecommerce('select_item', { item_list_name, item_list_id, items });
};

/** AddPaymentInfo — informou dados de pagamento */
export const trackAddPaymentInfo = (params = {}) => {
  const { _ga4Items = [], payment_type = '', ...p } = params;
  const base = { currency: 'BRL', value: 0, ...p };

  if (active('metaPixelId'))    meta('track', 'AddPaymentInfo', base);
  if (active('tiktokPixelId'))  tiktok('AddPaymentInfo', { value: base.value, currency: base.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_payment_info', ev: base.value, ec: base.currency });
  ga4Ecommerce('add_payment_info', { currency: base.currency, value: base.value, payment_type, items: _ga4Items });
};

/** AddShippingInfo — informou dados de entrega */
export const trackAddShippingInfo = (params = {}) => {
  const { _ga4Items = [], shipping_tier = '', ...p } = params;
  const base = { currency: 'BRL', value: 0, ...p };

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'add_shipping_info' });
  ga4Ecommerce('add_shipping_info', { currency: base.currency, value: base.value, shipping_tier, items: _ga4Items });
};

/** ViewPromotion — visualizou banner/promoção */
export const trackViewPromotion = (params = {}) => {
  const { _ga4Items = [], promotion_id = '', promotion_name = '', creative_name = '', creative_slot = '' } = params;

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'view_promotion' });
  ga4Ecommerce('view_promotion', { promotion_id, promotion_name, creative_name, creative_slot, items: _ga4Items });
};

/** SelectPromotion — clicou em banner/promoção */
export const trackSelectPromotion = (params = {}) => {
  const { _ga4Items = [], promotion_id = '', promotion_name = '', creative_name = '', creative_slot = '' } = params;

  if (isBrowser && window.uetq) window.uetq.push({ ea: 'select_promotion' });
  ga4Ecommerce('select_promotion', { promotion_id, promotion_name, creative_name, creative_slot, items: _ga4Items });
};

/** Subscribe — assinou newsletter ou serviço recorrente */
export const trackSubscribe = (params = {}) => {
  const base = { value: 0, currency: 'BRL', predicted_ltv: 0, ...params };

  if (active('metaPixelId'))    meta('track', 'Subscribe', base);
  if (active('tiktokPixelId'))  tiktok('Subscribe', { value: base.value, currency: base.currency });
  if (active('redditPixelId'))  reddit('Lead');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'subscribe', ev: base.value, ec: base.currency });
  ga4Event('generate_lead', { method: 'subscribe', currency: base.currency, value: base.value });
  gadsConversion(CONFIG.googleAdsConversions?.signup);
};

/** StartTrial — iniciou período de teste gratuito
 *  TikTok: usa evento padrão 'StartTrial' (não 'Subscribe') */
export const trackStartTrial = (params = {}) => {
  const base = { value: 0, currency: 'BRL', predicted_ltv: 0, ...params };

  if (active('metaPixelId'))    meta('track', 'StartTrial', base);
  if (active('tiktokPixelId'))  tiktok('StartTrial', { value: base.value, currency: base.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'start_trial' });
  ga4Event('generate_lead', { method: 'trial', currency: base.currency, value: base.value });
};

/** FindLocation — buscou localização física da empresa */
export const trackFindLocation = () => {
  if (active('metaPixelId'))    meta('track', 'FindLocation');
  if (active('tiktokPixelId'))  tiktok('FindLocation');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'find_location' });
  ga4Event('find_location');
};

/** Donate — fez uma doação */
export const trackDonate = (params = {}) => {
  const base = { value: 0, currency: 'BRL', ...params };

  if (active('metaPixelId'))    meta('track', 'Donate', base);
  if (active('redditPixelId'))  reddit('Purchase', { value: base.value, currency: base.currency });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'donate', ev: base.value, ec: base.currency });
  ga4Event('generate_lead', { method: 'donate', currency: base.currency, value: base.value });
};

/** SubmitApplication — enviou candidatura, inscrição ou formulário longo */
export const trackSubmitApplication = (params = {}) => {
  const base = { value: 0, currency: 'BRL', ...params };

  if (active('metaPixelId'))    meta('track', 'SubmitApplication', base);
  if (active('tiktokPixelId'))  tiktok('SubmitForm', { value: base.value, currency: base.currency });
  if (active('redditPixelId'))  reddit('Lead');
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'submit_application' });
  ga4Event('generate_lead', { method: 'application', currency: base.currency, value: base.value });
  gadsConversion(CONFIG.googleAdsConversions?.lead);
};

/** Download — download de arquivo (PDF, app, material) */
export const trackDownload = (params = {}) => {
  const base = { file_name: '', file_extension: '', link_url: '', ...params };

  if (active('tiktokPixelId'))  tiktok('Download', { content_name: base.file_name });
  if (isBrowser && window.uetq) window.uetq.push({ ea: 'file_download', el: base.file_name });
  ga4Event('file_download', { file_name: base.file_name, file_extension: base.file_extension, link_url: base.link_url });
};

/** CustomizeProduct — personalizou produto (cor, tamanho, gravação, etc.) */
export const trackCustomizeProduct = (params = {}) => {
  if (active('metaPixelId'))   meta('track', 'CustomizeProduct', params);
  if (active('tiktokPixelId')) tiktok('CustomizeProduct', { content_name: params.content_name || '' });
  ga4Event('customize_product', params);
};

// ============================================================
// EVENTO GENÉRICO CUSTOMIZADO
// ============================================================

/** Evento customizado — qualquer nome de evento não coberto acima */
export const trackCustom = (metaEventName, ga4EventName, params = {}) => {
  if (active('metaPixelId'))   window.fbq?.('trackCustom', metaEventName, params);
  if (active('tiktokPixelId')) window.ttq?.track(metaEventName, params);
  ga4Event(ga4EventName, params);
};

/** Scroll depth tracker — utilitário para medir profundidade de scroll */
export const scrollTracker = (threshold = 50, callback) => {
  let fired = false;
  return () => {
    if (fired) return;
    const pct = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
    if (pct >= threshold) { fired = true; callback(); }
  };
};
```

---

## PASSO 4 — Eventos por página e tipo de negócio

### Regra de ouro por plataforma (NUNCA violar)

| Plataforma | Regra crítica |
|---|---|
| **Meta** | `ViewContent` sempre no load da página — NUNCA em scroll. `Lead`/`Purchase` apenas quando ação é confirmada (onSubmit, não onClick) |
| **GA4** | Sempre `{ecommerce: null}` antes de qualquer evento de e-commerce |
| **TikTok** | `CompletePayment` = Purchase. `SubmitForm` = Lead. Nunca usar eventos Meta como nomes |
| **Google Ads** | Conversão só no evento real de conversão (Lead confirmado, Purchase confirmado). Não usar em PageView |
| **Geral** | `trackPageView()` em TODA rota via `useEffect(()=>{}, [])`. Em SPA nunca confiar no carregamento automático |

---

### 📄 TIPO 1 — Landing Page / Página de Captura (Infoproduto, Serviço, Imóvel)

**Funil:** Visita → Interesse → Lead (formulário ou WhatsApp)

```jsx
// Eventos no LOAD (automáticos)
// ✅ PageView → todas as plataformas
// ✅ ViewContent → todas as plataformas (sinaliza o que o usuário está vendo)

// Eventos na INTERAÇÃO (nunca automáticos)
// ✅ Lead → no onSubmit do formulário (NÃO no onClick do botão)
// ✅ Contact → no clique no botão de WhatsApp/telefone
// ✅ Schedule → no clique em "Agendar"
// ✅ trackCustom('FormStart') → no onFocus do primeiro campo (micro-conversão)
// ❌ NUNCA: ViewContent no scroll, Lead no onClick, PageView duplicado

import { useEffect } from 'react';
import {
  trackPageView, trackViewContent, trackLead,
  trackContact, trackSchedule, trackCustom, scrollTracker,
  readField, normalizePhone, splitName, isValidEmail, normalizeCity, // utilitários de formulário
} from '../tracking/tracking';

// ── Dados do conteúdo da página ────────────────────────────
const CONTEUDO = {
  content_ids:  ['ID_PRODUTO'],
  content_type: 'product',        // 'home_listing' para imóveis
  content_name: 'Nome do Produto / Serviço',
  value:        0,                // 0 se não tiver preço fixo; preço real se tiver
  currency:     'BRL',
  _ga4Items: [{ item_id: 'ID_PRODUTO', item_name: 'Nome do Produto', price: 0, quantity: 1 }],
};

export function LandingPage() {

  // ── LOAD: PageView + ViewContent (obrigatórios em toda landing page) ──
  useEffect(() => {
    trackPageView();
    trackViewContent(CONTEUDO);
  }, []);

  // ── SCROLL: micro-conversão de interesse (opcional, não substitui ViewContent) ──
  useEffect(() => {
    return scrollTracker([75], (pct) =>
      trackCustom('ScrollEngagement', { percent_scrolled: pct }));
  }, []);

  // ── INTERAÇÕES ──────────────────────────────────────────
  const onWhatsApp = async () => {
    await trackContact('whatsapp');           // Meta: Contact | GA4: generate_lead | TikTok: Contact
    window.open('https://wa.me/55SEUNUMERO', '_blank');
  };

  const onFormFocus = () =>
    trackCustom('FormStart', { form_name: 'captacao_lead' }); // sinaliza interesse em preencher

  const onSubmit = async (e) => {
    e.preventDefault();

    // ── Leitura e normalização dos campos ──────────────────────────────
    // readField() busca por id="X" ou name="X" — adaptar ao HTML real do site
    const emailRaw = readField('email');        // id="email" ou name="email"
    const nomeRaw  = readField('nome');         // id="nome" ou name="nome"
    const telRaw   = readField('telefone');     // id="telefone" ou name="telefone"

    // Validar email antes de disparar
    if (!isValidEmail(emailRaw)) return;        // ← não dispara se email inválido

    // Separar nome em firstName / lastName (alinha com campos fn/ln do Meta CAPI)
    const { firstName, lastName } = splitName(nomeRaw);

    // Normalizar telefone para formato Meta: somente dígitos + DDI
    const phone = normalizePhone(telRaw);       // ex: '5511999998888'

    await trackLead({                           // Meta: Lead | GA4: generate_lead | TikTok: SubmitForm
      email:        emailRaw,                   // → SHA256 no servidor (Meta Advanced Matching / Google Enhanced Conversions)
      phone,                                    // → já normalizado; SHA256 no servidor
      firstName,                                // → SHA256 no servidor (campo fn no Meta CAPI)
      lastName,                                 // → SHA256 no servidor (campo ln no Meta CAPI)
      content_name: CONTEUDO.content_name,
    });
    // redirecionar ou mostrar mensagem de sucesso
  };

  const onSchedule = () =>
    trackSchedule();                            // Meta: Schedule | GA4: generate_lead | TikTok: Schedule

  return (
    <div>
      <button onClick={onWhatsApp}>Falar no WhatsApp</button>
      {/* ── Campos com id E name ── */}
      <form onSubmit={onSubmit}>
        <input id="nome"      name="nome"      placeholder="Seu nome completo" required />
        <input id="email"     name="email"     type="email" placeholder="Seu e-mail" required onFocus={onFormFocus} />
        <input id="telefone"  name="telefone"  type="tel"   placeholder="Seu telefone" />
        <button type="submit">Quero saber mais</button>
      </form>
      <button onClick={onSchedule}>Agendar visita</button>
    </div>
  );
}

```

---

### 🏠 TIPO 2 — Página de Imóvel (Real Estate)

**Diferença:** `content_type: 'home_listing'` obrigatório para Meta Dynamic Ads imobiliário.

```jsx
const IMOVEL = {
  content_ids:    ['APT-SBC-001'],
  content_type:   'home_listing',            // ← obrigatório para catálogo Meta imobiliário
  content_name:   'Apartamento 3 Quartos - São Bernardo',
  city:           'São Bernardo do Campo',
  region:         'SP',
  country:        'BR',
  neighborhood:   'Bairro Nobre',
  value:          850000,
  currency:       'BRL',
  property_size:  260,
  num_baths:      2,
  num_beds:       3,
  parking_spaces: 3,
  _ga4Items: [{
    item_id: 'APT-SBC-001', item_name: 'Apartamento 3 Quartos',
    item_category: 'Residencial', item_category2: 'São Bernardo do Campo',
    price: 850000, quantity: 1
  }],
};

export function PaginaImovel() {
  useEffect(() => {
    trackPageView();
    trackViewContent(IMOVEL);               // content_type: 'home_listing' → Dynamic Ads imobiliário
  }, []);

  // Scroll 75%: sinaliza leitura profunda (interesse qualificado)
  useEffect(() => {
    return scrollTracker([75], (pct) =>
      trackCustom('ImovelLido', { content_name: IMOVEL.content_name, scroll_depth: pct }));
  }, []);

  const onWhatsApp = async () => { await trackContact('whatsapp'); window.open('https://wa.me/55...', '_blank'); };
  const onSubmit   = async (e, d) => { e.preventDefault(); await trackLead({ ...d, content_name: IMOVEL.content_name }); };
  const onSchedule = () => trackSchedule();
}
```

---

### 🛒 TIPO 3 — E-commerce: Página de Listagem de Produtos

**Funil:** Listagem → Produto → Carrinho → Checkout → Confirmação

```jsx
// ── Listagem (Category / Collection page) ──────────────────
// ✅ PageView no load
// ✅ ViewItemList no load (GA4 e Bing — lista de produtos visíveis)
// ✅ SelectItem no clique em qualquer produto
// ✅ ViewPromotion se houver banner de promoção visível no load
// ✅ SelectPromotion no clique no banner

import {
  trackPageView, trackViewItemList, trackSelectItem,
  trackViewPromotion, trackSelectPromotion, trackSearch
} from '../tracking/tracking';

export function ListagemProdutos({ produtos, categoria }) {
  useEffect(() => {
    trackPageView();
    trackViewItemList({
      item_list_id:   categoria.id,
      item_list_name: categoria.nome,
      items: produtos.map((p, i) => ({
        item_id: p.id, item_name: p.nome,
        item_category: categoria.nome, price: p.preco,
        index: i, quantity: 1,
      })),
    });
    // Se tiver banner de promoção visível na página:
    trackViewPromotion({ promotion_id: 'banner_topo', promotion_name: 'Oferta da Semana' });
  }, []);

  const onClicarProduto = (produto, index) => {
    trackSelectItem({
      item_list_id:   categoria.id,
      item_list_name: categoria.nome,
      items: [{ item_id: produto.id, item_name: produto.nome, price: produto.preco, index }],
    });
    // navegar para página do produto
  };

  const onClicarBanner = () =>
    trackSelectPromotion({ promotion_id: 'banner_topo', promotion_name: 'Oferta da Semana' });

  const onPesquisar = (termo) =>
    trackSearch(termo);                      // Meta: Search | GA4: search | TikTok: Search

  return (/* jsx */);
}
```

---

### 🛍️ TIPO 4 — E-commerce: Página de Produto

```jsx
// ✅ PageView + ViewContent no load (obrigatório)
// ✅ AddToCart no clique em "Adicionar ao carrinho"
// ✅ AddToWishlist no clique em "Favoritar"
// ✅ CustomizeProduct se houver seleção de variante (cor, tamanho)
// ❌ NUNCA: AddToCart automático no load

import {
  trackPageView, trackViewContent, trackAddToCart,
  trackAddToWishlist, trackCustomizeProduct
} from '../tracking/tracking';

export function PaginaProduto({ produto }) {
  useEffect(() => {
    trackPageView();
    trackViewContent({
      content_ids:  [produto.id],
      content_type: 'product',
      content_name: produto.nome,
      value:        produto.preco,
      currency:     'BRL',
      _ga4Items: [{ item_id: produto.id, item_name: produto.nome, price: produto.preco, quantity: 1 }],
    });
  }, []);

  const onAddToCart = (quantidade = 1) => {
    trackAddToCart({
      content_ids:  [produto.id],
      content_name: produto.nome,
      value:        produto.preco * quantidade,
      currency:     'BRL',
      num_items:    quantidade,
      _ga4Items: [{ item_id: produto.id, item_name: produto.nome, price: produto.preco, quantity: quantidade }],
    });
  };

  const onFavoritar = () =>
    trackAddToWishlist({
      content_ids:  [produto.id],
      content_name: produto.nome,
      value:        produto.preco,
      currency:     'BRL',
    });

  const onSelecionarVariante = (variante) =>
    trackCustomizeProduct({ content_name: produto.nome, variante });

  return (/* jsx */);
}
```

---

### 🛒 TIPO 5 — E-commerce: Página do Carrinho

```jsx
// ✅ PageView + ViewCart no load
// ✅ RemoveFromCart ao remover item
// ✅ InitiateCheckout no clique em "Finalizar compra"
// ❌ NUNCA: Purchase aqui — apenas na confirmação

import {
  trackPageView, trackViewCart, trackRemoveFromCart, trackInitiateCheckout
} from '../tracking/tracking';

export function Carrinho({ itens, total }) {
  useEffect(() => {
    trackPageView();
    trackViewCart({
      value:    total,
      currency: 'BRL',
      _ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
    });
  }, []);

  const onRemoverItem = (item) =>
    trackRemoveFromCart({
      content_name: item.nome,
      value:        item.preco * item.qtd,
      currency:     'BRL',
      _ga4Items: [{ item_id: item.id, item_name: item.nome, price: item.preco, quantity: item.qtd }],
    });

  const onFinalizarCompra = () => {
    trackInitiateCheckout({
      value:    total,
      currency: 'BRL',
      num_items: itens.length,
      content_ids: itens.map(i => i.id),
      _ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
    });
    // navegar para checkout
  };

  return (/* jsx */);
}
```

---

### 💳 TIPO 6 — E-commerce: Página de Checkout

```jsx
// ✅ PageView no load
// ✅ AddShippingInfo ao confirmar endereço de entrega
// ✅ AddPaymentInfo ao informar dados de pagamento
// ❌ NUNCA: Purchase aqui — apenas na confirmação do pedido

import {
  trackPageView, trackAddShippingInfo, trackAddPaymentInfo
} from '../tracking/tracking';

export function Checkout({ itens, total }) {
  useEffect(() => { trackPageView(); }, []);

  const onConfirmarEndereco = () =>
    trackAddShippingInfo({
      value:    total,
      currency: 'BRL',
      _ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
    });

  const onInformarPagamento = (metodoPagamento) =>
    trackAddPaymentInfo({
      value:          total,
      currency:       'BRL',
      payment_type:   metodoPagamento,      // 'credit_card', 'pix', 'boleto'
      _ga4Items: itens.map(i => ({ item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd })),
    });

  return (/* jsx */);
}
```

---

### ✅ TIPO 7 — E-commerce: Página de Confirmação / Obrigado (Compra)

```jsx
// ✅ PageView + Purchase no load — UMA VEZ (usar order_id único para deduplicação)
// ✅ Purchase com value, currency e order_id obrigatórios
// ❌ NUNCA: Purchase em qualquer outra página além desta
// ❌ NUNCA: sem order_id (causa duplicação nos relatórios)

import { useEffect, useRef } from 'react';
import { trackPageView, trackPurchase } from '../tracking/tracking';

export function PaginaObrigado({ pedido }) {
  const fired = useRef(false);              // proteção contra double-fire no StrictMode

  useEffect(() => {
    if (fired.current) return;
    fired.current = true;

    trackPageView();
    trackPurchase({
      order_id:     pedido.id,             // OBRIGATÓRIO — evita duplicatas browser + servidor
      value:        pedido.total,          // OBRIGATÓRIO
      currency:     'BRL',                 // OBRIGATÓRIO
      content_ids:  pedido.itens.map(i => i.id),
      email:        pedido.email,          // Enhanced Matching Meta + Google Enhanced Conversions
      phone:        pedido.telefone,
      firstName:    pedido.nome,
      _ga4Items: pedido.itens.map(i => ({
        item_id: i.id, item_name: i.nome, price: i.preco, quantity: i.qtd
      })),
    });
  }, []);

  return (/* jsx */);
}
```

---

### 🎓 TIPO 8 — Infoproduto / SaaS: Página de Obrigado (Lead)

```jsx
// Quando o lead é capturado em outra página e confirmado aqui
// ✅ PageView + Lead no load (se não foi disparado no formulário anterior)
// ✅ SignUp se for cadastro de conta

import { useEffect, useRef } from 'react';
import { trackPageView, trackLead, trackSignUp } from '../tracking/tracking';

export function ObrigadoLead({ dados }) {
  const fired = useRef(false);

  useEffect(() => {
    if (fired.current) return;
    fired.current = true;

    trackPageView();
    // Disparar Lead AQUI apenas se não foi disparado no formulário anterior
    // Se o formulário já chamou trackLead(), NÃO chamar novamente aqui
    trackLead({
      email:        dados.email,
      phone:        dados.phone,
      firstName:    dados.nome,
      content_name: 'Lead Capturado',
      value:        0,
      currency:     'BRL',
    });
  }, []);

  return (/* jsx */);
}
```

---

### 📧 TIPO 9 — Página de Assinatura / Newsletter / SaaS Trial

```jsx
// ✅ PageView + ViewContent no load
// ✅ Subscribe no onSubmit do formulário de assinatura
// ✅ StartTrial no início de período de teste
// ✅ SignUp na criação de conta

import {
  trackPageView, trackViewContent, trackSubscribe, trackStartTrial, trackSignUp
} from '../tracking/tracking';

export function PaginaAssinatura({ plano }) {
  useEffect(() => {
    trackPageView();
    trackViewContent({ content_name: plano.nome, value: plano.preco, currency: 'BRL' });
  }, []);

  const onAssinar = async (e, dados) => {
    e.preventDefault();
    await trackSubscribe({ value: plano.preco, currency: 'BRL', email: dados.email });
  };

  const onIniciarTrial = async (e, dados) => {
    e.preventDefault();
    await trackStartTrial({ value: 0, currency: 'BRL', email: dados.email });
  };

  const onCriarConta = (metodo = 'email') => trackSignUp(metodo);

  return (/* jsx */);
}
```

---

### 📰 TIPO 10 — Blog / Conteúdo / Artigo

```jsx
// ✅ PageView no load
// ✅ ViewContent no load (o artigo é o conteúdo)
// ✅ ScrollTracker para medir engajamento com o conteúdo
// ✅ Download se houver material para baixar
// ✅ Lead se houver formulário de newsletter inline

import {
  trackPageView, trackViewContent, trackDownload, trackLead, scrollTracker
} from '../tracking/tracking';

export function PaginaArtigo({ artigo }) {
  useEffect(() => {
    trackPageView();
    trackViewContent({
      content_ids:  [artigo.slug],
      content_type: 'article',
      content_name: artigo.titulo,
    });
  }, []);

  useEffect(() => {
    // Medir profundidade de leitura — útil para remarketing de leitores engajados
    return scrollTracker([25, 50, 75, 90], (pct) =>
      trackCustom('LeituraArtigo', { artigo: artigo.slug, scroll_depth: pct }));
  }, []);

  const onDownload = (arquivo) =>
    trackDownload({ content_name: arquivo.nome, content_ids: [arquivo.id] });

  const onNewsletterSubmit = async (e, email) => {
    e.preventDefault();
    await trackLead({ email, content_name: 'Newsletter', value: 0, currency: 'BRL' });
  };

  return (/* jsx */);
}
```

---

### 📍 TIPO 11 — Página de Localização / Franquia / Loja Física

```jsx
// ✅ PageView + ViewContent no load
// ✅ FindLocation no clique em "Ver no mapa" ou "Como chegar"
// ✅ Contact no clique em WhatsApp/telefone da unidade

import {
  trackPageView, trackViewContent, trackFindLocation, trackContact
} from '../tracking/tracking';

export function PaginaUnidade({ unidade }) {
  useEffect(() => {
    trackPageView();
    trackViewContent({ content_name: unidade.nome, content_type: 'local' });
  }, []);

  const onVerMapa  = () => trackFindLocation();
  const onWhatsApp = async () => { await trackContact('whatsapp'); window.open(unidade.whatsapp); };

  return (/* jsx */);
}
```

---

### 🔁 SEQUÊNCIA CORRETA DO FUNIL DE E-COMMERCE (visão geral)

```
Listagem       → PageView + ViewItemList
    ↓ clique
Produto        → PageView + ViewContent
    ↓ ação
Carrinho       → PageView + ViewCart + AddToCart (vem do clique anterior)
    ↓ ação
Checkout       → PageView + InitiateCheckout + AddShippingInfo + AddPaymentInfo
    ↓ confirmação
Obrigado       → PageView + Purchase (com order_id único)
```

### 🔁 SEQUÊNCIA CORRETA DO FUNIL DE LEAD (visão geral)

```
Landing Page   → PageView + ViewContent
    ↓ interação
                 scrollTracker (micro-conversão de interesse)
                 onFocus formulário → trackCustom('FormStart')
    ↓ submit
                 onSubmit → trackLead() OU trackContact()
    ↓ redirect
Obrigado        → PageView (+ Lead apenas se não foi disparado no formulário)
```

---

## PASSO 5 — Mapeamento de eventos por plataforma

### 📄 Eventos de Navegação

| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|---|---|---|---|---|---|---|
| `trackPageView()` | PageView | page_view | PageView | pagevisit | pageLoad | PageVisit |
| `trackViewContent(params)` | ViewContent | view_item | ViewContent | viewcategory | view_content | ViewContent |
| `trackSearch(query)` | Search | search | Search | — | search | Search |
| `trackFindLocation()` | FindLocation | find_location | FindLocation | — | find_location | — |

### 🛒 Eventos de E-commerce

| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|---|---|---|---|---|---|---|
| `trackAddToCart(params)` | AddToCart | add_to_cart | AddToCart | addtocart | add_to_cart | AddToCart |
| `trackAddToWishlist(params)` | AddToWishlist | add_to_wishlist | AddToWishlist | — | add_to_wishlist | — |
| `trackRemoveFromCart(params)` | — | remove_from_cart | — | — | remove_from_cart | — |
| `trackViewCart(params)` | — | view_cart | — | — | view_cart | — |
| `trackViewItemList(params)` | — | view_item_list | — | — | view_item_list | — |
| `trackSelectItem(params)` | — | select_item | — | — | select_item | — |
| `trackInitiateCheckout(params)` | InitiateCheckout | begin_checkout | InitiateCheckout | checkout | initiate_checkout | AddToCart |
| `trackAddPaymentInfo(params)` | AddPaymentInfo | add_payment_info | AddPaymentInfo | — | add_payment_info | — |
| `trackAddShippingInfo(params)` | — | add_shipping_info | — | — | add_shipping_info | — |
| `trackPurchase(params)` | Purchase | purchase | CompletePayment *(+ ttq.identify())* | checkout | purchase | Purchase |
| `trackCustomizeProduct(params)` | CustomizeProduct | customize_product | CustomizeProduct | — | — | — |

### 🏷️ Eventos de Promoção

| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|---|---|---|---|---|---|---|
| `trackViewPromotion(params)` | — | view_promotion | — | — | view_promotion | — |
| `trackSelectPromotion(params)` | — | select_promotion | — | — | select_promotion | — |

### 🎯 Eventos de Conversão / Lead

| Função CDP Edge | Meta | GA4 | TikTok | Pinterest | Bing | Reddit |
|---|---|---|---|---|---|---|
| `trackLead(params)` | Lead | generate_lead | Lead *(+ ttq.identify())* | lead | submit_form | Lead |
| `trackContact(params)` | Contact | generate_lead | Contact | lead | contact | Lead |
| `trackSchedule(params)` | Schedule | generate_lead | Schedule | lead | schedule | Lead |
| `trackSignUp(params)` | CompleteRegistration | sign_up | CompleteRegistration | signup | sign_up | SignUp |
| `trackSubscribe(params)` | Subscribe | — | Subscribe | — | subscribe | Lead |
| `trackStartTrial(params)` | StartTrial | — | StartTrial | — | start_trial | — |
| `trackSubmitApplication(params)` | SubmitApplication | — | SubmitForm | — | submit_application | Lead |
| `trackDonate(params)` | Donate | — | — | — | donate | Purchase |
| `trackDownload(params)` | — | file_download | Download | — | download | — |

### ⚙️ Utilitários

| Função CDP Edge | Descrição |
|---|---|
| `trackCustom(name, params)` | Evento personalizado em todas as plataformas ativas |
| `scrollTracker(thresholds, cb)` | Dispara callback em % de scroll (ex: 25%, 50%, 75%, 90%) |
| `getCityFromGeolocation()` | Captura cidade do usuário via GPS + OpenStreetMap (gratuito) |
| `sendServerEvent(name, params)` | Envia evento direto ao servidor (fire-and-forget, não bloqueia) |
| `getUTMs()` | Retorna `{ utm_source, utm_medium, utm_campaign, utm_content, utm_term }` da URL — para rastreamento interno e CRM (independente da atribuição das plataformas) |
| `getUserId()` | Retorna User ID first-party persistente (cookie 365 dias) — identifica o mesmo usuário entre sessões sem depender de cookies de terceiros |
| `passCheckoutParams(options?)` | Injeta UTMs + User ID nos links de checkout externo (Hotmart, Kiwify, Eduzz, Monetizze, CartPanda). Hotmart: `xcod` + `sck` (UTMs pipe-separados). Kiwify/Eduzz: `src`. Usa MutationObserver para links carregados dinamicamente. Chamar no `useEffect` do layout ou no `<body onload>` |
| `normalizePhone(tel)` | Remove caracteres não-numéricos e adiciona DDI Brasil `55`. Formato exigido pela Meta: `'5511999998888'`. Retorna `undefined` se vazio |
| `normalizeCity(city)` | Normaliza cidade para campo `ct` do Meta Advanced Matching: lowercase, remove acentos e caracteres especiais. Ex: `'São Paulo'` → `'saopaulo'`. Deve ser aplicada ANTES do SHA256 no servidor (CAPI) |
| `splitName(fullName)` | Divide nome completo em `{ firstName, lastName }` — alinha com campos `fn`/`ln` do Meta CAPI e Enhanced Conversions do Google |
| `isValidEmail(email)` | Valida formato de email — retorna `true/false`. Usar como guarda antes de `trackLead()` |
| `readField(selector)` | Lê valor de campo de formulário por `id`, `name` ou seletor CSS. Usa fallback automático entre os dois |
| `fixItems(raw, fieldMap?)` | Converte items MAL FORMATADOS para o padrão GA4. Recebe objeto ou array com qualquer estrutura de campo |
| `buildItems(params)` | Monta array GA4 a partir de parâmetros SOLTOS (product_id, product_name, price, etc.) |
| `ga4ItemsToMeta(items)` | Converte GA4 items → `{ content_ids, contents, num_items, value }` do Meta Pixel (Universal Conversion). Cada `contents[i]` inclui `id`, `quantity`, `item_price`, `title`, `category`, `brand` — formato completo exigido pelo Meta para e-commerce |
| `ga4ItemsToTikTok(items)` | Converte GA4 items → `{ contents, value }` do TikTok Pixel (Universal Conversion) |

### 📋 Parâmetros comuns

| Parâmetro | Tipo | Usado em |
|---|---|---|
| `value` | number | Purchase, Lead, Subscribe, Donate, etc. |
| `currency` | string | Purchase, AddToCart, etc. (ex: `'BRL'`) |
| `content_ids` | string[] | ViewContent, AddToCart, Purchase |
| `content_name` | string | ViewContent, AddToCart, etc. |
| `content_type` | string | ViewContent (`'product'`, `'home_listing'`, etc.) |
| `order_id` | string | Purchase (único por pedido — evita duplicatas) |
| `email` | string | Lead, Purchase (será hashed SHA256 no server-side) |
| `phone` | string | Lead, Purchase (será hashed SHA256 no server-side) |
| `firstName` | string | Lead, Purchase (será hashed SHA256 no server-side) |
| `items` | object[] | Eventos GA4 de e-commerce (name, id, price, quantity) |
| `search_term` | string | trackSearch |
| `promotion_id` | string | trackViewPromotion, trackSelectPromotion |
| `item_list_id` | string | trackViewItemList, trackSelectItem |

---

## CHECKLIST ANTI-ERROS

- [ ] `ViewContent` no **carregamento** da página — nunca no scroll
- [ ] Botão de CTA/orçamento → `trackLead()` ou `trackContact()`, **nunca** AddToWishlist
- [ ] `trackPageView()` em **toda** página via `useEffect(()=>{}, [])`
- [ ] `send_page_view: false` no gtag (SPA) — `true` / ausente em HTML puro
- [ ] `value` + `currency` obrigatórios no `trackPurchase()`
- [ ] `order_id` único em Purchase (evita duplicata)
- [ ] Formulários com `trackLead()` no `onSubmit` (não no onClick do botão)
- [ ] Credenciais null no config → plataforma ignorada sem erros
- [ ] GA4 com tag ativa além do ID (erro clássico: ID configurado como variável mas sem tag)
- [ ] `trackLead()` e `trackContact()` são **async** — usar `await` ou não bloquear a UX
- [ ] Geolocalização é opcional para o usuário — se negar, o evento dispara normalmente sem `city`
- [ ] Nunca usar geolocalização como **condição de disparo** — sempre disparar o evento, capturar cidade como dado extra
- [ ] No Modo A (Aplicação Direta): nunca sobrescrever arquivos inteiros — usar `Edit` para inserir blocos cirurgicamente
- [ ] UTMs são capturados automaticamente na chegada — para rastreamento interno usar `getUTMs()` e incluir no payload enviado ao servidor
- [ ] UTMs **NÃO** são enviados para as plataformas de anúncio diretamente — servem para rastreamento interno (D1, CRM) independente da janela de atribuição das plataformas
- [ ] Página SPA: UTMs capturados no primeiro carregamento são preservados mesmo após navegação entre rotas (ficam em `_utms` na memória)
- [ ] `gclid`, `wbraid`, `gbraid` capturados da URL → atribuição a nível de anúncio do Google Ads
- [ ] `ttclid` capturado da URL → atribuição a nível de anúncio do TikTok Ads
- [ ] Enhanced Conversions (Google Ads): passar `email` e `phone` no `trackLead()` e `trackPurchase()` para ativar atribuição a nível de usuário
- [ ] `allow_enhanced_conversions: true` na config do `gtag('config', 'AW-...')` — obrigatório para Enhanced Conversions funcionar
- [ ] User ID first-party (`getUserId()`) — incluir em payloads de servidor para identificação cross-session

---

## PASSO 6 — MODO SERVER-SIDE (quando solicitado)

> **Quando usar:** o usuário pede tracking server-side, CAPI, Measurement Protocol, "evitar bloqueio de adblocker", ou quer o endpoint `/track`.

### Por que server-side existe

Adblockers e o ITP do Safari bloqueiam ou limitam pixels no browser. O server-side envia o mesmo evento direto das APIs das plataformas, sem depender do navegador. As plataformas deduplicam pelo `event_id` — o mesmo ID enviado pelo browser e pelo servidor.

### Arquitetura com server-side

```
Browser (tracking.js)
  ├── dispara evento client-side (Meta Pixel, GA4, TikTok...)
  ├── gera event_id único
  ├── lê cookies: _fbp, _fbc, _ttp, _ga
  ├── captura UTMs da URL (utm_source, utm_medium, utm_campaign, utm_content, utm_term)
  └── POST /track → { event_name, event_id, cookies, utms, user_data, page_url }

Servidor Node.js (/track)
  ├── recebe payload do browser
  ├── adiciona: IP real, User-Agent do header
  ├── SHA256 hash: email, phone, nome, cidade, estado, CEP, país
  └── envia simultaneamente para:
      ├── Meta CAPI        (graph.facebook.com)
      ├── GA4 MP           (apenas purchase/refund)
      └── TikTok Events API
```

---

### PASSO 6.1 — Atualizar `tracking.config.js` com credenciais server-side

```js
const TRACKING_CONFIG = {
  // ... credenciais existentes ...

  // ── SERVER-SIDE (backend) ──────────────────────────────
  server: {
    metaCapi: {
      accessToken: null,           // Meta CAPI Access Token (gerado no Events Manager)
      testEventCode: null,         // ex: 'TEST12345' — só durante testes
    },
    ga4: {
      apiSecret: null,             // GA4 Measurement Protocol API Secret
      // measurement_id já usa ga4Id acima
    },
    tiktok: {
      accessToken: null,           // TikTok Events API Access Token
      testEventCode: null,
    },
  },
};
```

---

### PASSO 6.2 — Atualizar `tracking.js` para coletar cookies e enviar ao servidor

Adicionar ao `tracking.js` a função `sendServerEvent` que coleta os cookies do browser e faz POST ao endpoint:

```js
// ── Captura cookies do browser para repassar ao servidor ──
// Cookies gerados pelas plataformas (identificam usuário e anúncio de origem)
const getCookies = () => ({
  // Meta
  fbp:      document.cookie.match(/_fbp=([^;]+)/)?.[1]    || '',  // ID do browser pelo Meta Pixel
  fbc:      document.cookie.match(/_fbc=([^;]+)/)?.[1]    || '',  // Click ID Meta (gerado do fbclid)
  // TikTok
  ttp:      document.cookie.match(/_ttp=([^;]+)/)?.[1]    || '',  // ID do browser pelo TikTok Pixel
  // Google Analytics
  ga:       document.cookie.match(/_ga=([^;]+)/)?.[1]     || '',  // Raw _ga cookie
  // GA4 sessão — necessário para GA4 Measurement Protocol em webhooks externos (PASSO 2.23)
  // _ga_PROPERTY: "GS1.1.{session_id}.{session_num}.1.{last}.0.0.0"
  gaClient: (document.cookie.match(/_ga=([^;]+)/)?.[1] || '').replace(/^GA\d+\.\d+\./, ''), // client_id limpo
  gaSessionId:  (document.cookie.match(/_ga_[^=]+=([^;]+)/)?.[1] || '').replace(/^GS\d+\.\d+\./, '').split('.')[0] || '',
  gaSessionNum: (document.cookie.match(/_ga_[^=]+=([^;]+)/)?.[1] || '').replace(/^GS\d+\.\d+\./, '').split('.')[1] || '',
  // Google Ads
  gcl_aw:   document.cookie.match(/_gcl_aw=([^;]+)/)?.[1] || '', // Click ID Google Ads (do gclid)

  // Click IDs da URL (capturados no carregamento — não ficam em cookies por padrão)
  ctwaClid: _ctwaClid,  // Meta Click-to-WhatsApp
  gclid:    _gclid,     // Google Ads standard
  wbraid:   _wbraid,    // Google Ads (iOS / privacy preserving)
  gbraid:   _gbraid,    // Google Ads (app campaigns)
  ttclid:   _ttclid,    // TikTok Ads

  // User ID first-party (persistente entre sessões, não bloqueado por ad-blockers)
  userId:   _userId,
});

// ── Envio server-side (fire and forget — não bloqueia UX) ──
const sendServerEvent = (eventName, eventId, payload = {}) => {
  if (!isBrowser) return;
  // Não usar await — deixar rodar em paralelo com o event client-side
  fetch('/track', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      event_name: eventName,
      event_id:   eventId,
      page_url:   window.location.href,
      referrer:   document.referrer,
      cookies:    getCookies(),
      utms:       _utms,   // UTMs para rastreamento interno (atribuição própria)
      ...payload,
    }),
  }).catch(() => {}); // silencia erros de rede
};

// ── Gerador de event_id único (compartilhado browser ↔ servidor) ──
const genId = () => `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
```

Atualizar `trackLead`, `trackPurchase` e `trackContact` para gerar `event_id` e chamar `sendServerEvent` em paralelo:

```js
export const trackLead = async (params = {}) => {
  const eventId = genId();
  const cidade  = await getCidade();
  const base    = { value: 0, currency: 'BRL', ...params, ...(cidade && { city: cidade }) };

  // Client-side (existente)
  if (active('metaPixelId'))   window.fbq('track', 'Lead', base, { eventID: eventId });
  // ... demais plataformas ...

  // Server-side (paralelo)
  sendServerEvent('Lead', eventId, {
    value: base.value, currency: base.currency,
    content_name: base.content_name || '',
    city: cidade || '',
    // email/phone: se disponíveis no formulário, passar via params
    email: params.email || '',
    phone: params.phone || '',
  });
};
```

> **Regra:** `eventID` deve ser passado como 3º argumento do `fbq('track', ...)` para deduplicação Meta. TikTok usa `event_id` nas `properties`.

---

### PASSO 6.3 — Endpoint `/track` (Node.js / Next.js API Route)

```js
// Node.js Express: src/track.js
// Next.js App Router: app/track/route.js
// Next.js Pages Router: pages/track.js

import crypto from 'crypto';
import CONFIG from '../tracking/tracking.config.js';  // ajustar path

// SHA256 com lowercase e trim — padrão exigido pela Meta e TikTok
const sha256 = (val) => val
  ? crypto.createHash('sha256').update(String(val).toLowerCase().trim()).digest('hex')
  : '';

// IP real — checka proxies e load balancers
const getRealIp = (req) =>
  req.headers['x-forwarded-for']?.split(',')[0].trim() ||
  req.headers['x-real-ip'] ||
  req.socket?.remoteAddress ||
  req.ip || '';

// ── HANDLER PRINCIPAL ──
export async function POST(req) {                    // Next.js App Router
// export default async function handler(req, res) { // Express / Pages Router

  const body      = await req.json();               // Next.js App Router
  // const body   = req.body;                       // Express / Pages Router

  const {
    event_name, event_id, page_url, referrer,
    cookies = {},
    utms = {},            // UTMs para rastreamento interno (atribuição própria)
    email = '', phone = '', firstName = '', lastName = '',
    city = '', state = '', zipCode = '', country = 'br',
    value = 0, currency = 'BRL', content_name = '',
    content_ids = [], content_type = 'product',
    ga_client_id = '',    // extraído do cookie _ga no browser
    method = '',
  } = body;

  // Click IDs — identificam o anúncio que originou o evento (atribuição a nível de anúncio)
  const ctwaClid = cookies.ctwaClid || '';   // Meta Click-to-WhatsApp
  const gclid    = cookies.gclid    || '';   // Google Ads standard click ID → Enhanced Conversions
  const wbraid   = cookies.wbraid   || '';   // Google Ads (iOS / privacy preserving)
  const gbraid   = cookies.gbraid   || '';   // Google Ads (app campaigns)
  const ttclid   = cookies.ttclid   || '';   // TikTok Ads click ID
  const userId   = cookies.userId   || '';   // User ID first-party (cross-session)

  // UTMs extraídos — disponíveis para salvar em CRM/banco com cada evento
  // Exemplo: salvar no D1 → { event_name, utm_source, utm_campaign, email, value, page_url }
  const { utm_source = '', utm_medium = '', utm_campaign = '', utm_content = '', utm_term = '' } = utms;

  const userAgent  = req.headers['user-agent'] || '';
  const ip         = getRealIp(req);
  const eventTime  = Math.floor(Date.now() / 1000);

  const results = await Promise.allSettled([
    sendMetaCapi({ event_name, event_id, page_url, ip, userAgent,
                   cookies, email, phone, firstName, lastName,
                   city, state, zipCode, country,
                   value, currency, content_name, content_ids, content_type,
                   ctwaClid, userId, eventTime }),
    sendGA4Mp({ event_name, event_id, page_url,
                value, currency, content_name, ga_client_id, eventTime }),
    sendTiktokApi({ event_name, event_id, page_url, ip, userAgent,
                    cookies, email, phone, userId, value, currency, content_name, eventTime }),
  ]);

  // Next.js App Router
  return Response.json({ ok: true });
  // Express: res.json({ ok: true });
}

// ══════════════════════════════════════════════════════════
// META CONVERSIONS API
// ══════════════════════════════════════════════════════════
async function sendMetaCapi(p) {
  if (!CONFIG.metaPixelId || !CONFIG.server?.metaCapi?.accessToken) return;

  // user_data — todos os campos que existirem com SHA256
  // REGRA META: normalizar ANTES de hashear. Ordem importa:
  //   email   → lowercase + trim → sha256
  //   phone   → apenas dígitos (DDI incluído, ex: 5511999998888) → sha256
  //   fn / ln → lowercase + trim → sha256
  //   ct      → lowercase, sem acentos, sem espaços, sem caracteres especiais → sha256
  //             ex: 'São Paulo' → 'saopaulo' → sha256('saopaulo')
  //   st      → código 2 letras lowercase (ex: 'sp', 'rj') → sha256
  //   zp      → apenas dígitos (sem hífen) → sha256
  //   country → código ISO 2 letras lowercase (ex: 'br') → sha256
  //   fbp / fbc / ctwa_clid / external_id → NÃO hashear
  //   external_id → sha256 do User ID first-party (_cdp_uid)

  // normaliza cidade: lowercase, remove acentos, remove tudo que não é a-z0-9
  const normalizeCity = (city = '') =>
    String(city).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');

  const user_data = {
    client_ip_address: p.ip,                            // obrigatório quando action_source=website
    client_user_agent: p.userAgent,                     // obrigatório quando action_source=website
    ...(p.cookies.fbp  && { fbp:         p.cookies.fbp }),   // cookie _fbp — não hashear
    ...(p.cookies.fbc  && { fbc:         p.cookies.fbc }),   // cookie _fbc — não hashear
    // ctwa_clid: Click ID de anúncio Click-to-WhatsApp — não hashear, enviar raw
    // Permite que a Meta atribua o evento ao anúncio CTWA específico
    ...(p.ctwaClid     && { ctwa_clid:   p.ctwaClid }),
    // external_id: User ID first-party — sha256 do _cdp_uid cookie
    // Permite deduplicação cross-dispositivo e atribuição cross-session
    ...(p.userId       && { external_id: sha256(p.userId) }),
    ...(p.email        && { em:          sha256(p.email.toLowerCase().trim()) }),
    ...(p.phone        && { ph:          sha256(p.phone.replace(/\D/g, '')) }),
    ...(p.firstName    && { fn:          sha256(p.firstName.toLowerCase().trim()) }),
    ...(p.lastName     && { ln:          sha256(p.lastName.toLowerCase().trim()) }),
    ...(p.city         && { ct:          sha256(normalizeCity(p.city)) }),
    ...(p.state        && { st:          sha256(p.state.toLowerCase().trim()) }),
    ...(p.zipCode      && { zp:          sha256(p.zipCode.replace(/\D/g, '')) }),
    ...(p.country      && { country:     sha256(p.country.toLowerCase().trim()) }),
  };

  const custom_data = {
    value:        p.value,
    currency:     p.currency,
    content_name: p.content_name,
    content_ids:  p.content_ids,
    content_type: p.content_type,
  };

  const serverEvent = {
    event_name:       p.event_name,
    event_time:       p.eventTime,
    event_id:         p.event_id,                      // deduplicação com browser
    action_source:    'website',                       // obrigatório
    event_source_url: p.page_url,                     // obrigatório quando action_source=website
    user_data,
    custom_data,
  };

  const url = `https://graph.facebook.com/v25.0/${CONFIG.metaPixelId}/events`;
  const params = new URLSearchParams({ access_token: CONFIG.server.metaCapi.accessToken });
  if (CONFIG.server.metaCapi.testEventCode) {
    serverEvent.test_event_code = CONFIG.server.metaCapi.testEventCode;
  }

  await fetch(`${url}?${params}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ data: [serverEvent] }),
  });
}

// ══════════════════════════════════════════════════════════
// GA4 MEASUREMENT PROTOCOL
// Nota: GA4 MP só é confiável para Purchase e eventos com transaction_id.
// Para outros eventos, o GA4 coleta via gtag no browser é suficiente.
// ══════════════════════════════════════════════════════════
async function sendGA4Mp(p) {
  if (!CONFIG.ga4Id || !CONFIG.server?.ga4?.apiSecret) return;
  // GA4 MP só para purchase (padrão PYS e recomendação Google)
  if (p.event_name !== 'purchase') return;

  // client_id vem do cookie _ga: GA1.1.XXXXXXXXXX.YYYYYYYYYY → pegar as partes
  const clientId = p.ga_client_id
    ? p.ga_client_id.replace(/^GA\d+\.\d+\./, '')  // remove prefixo GA1.1.
    : `${Date.now()}.${Math.floor(Math.random()*1e9)}`;

  const url = `https://www.google-analytics.com/mp/collect` +
    `?measurement_id=${CONFIG.ga4Id}&api_secret=${CONFIG.server.ga4.apiSecret}`;

  await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: clientId,
      events: [{
        name: 'purchase',
        params: {
          transaction_id: p.event_id,
          value:          p.value,
          currency:       p.currency,
          engagement_time_msec: 100,
        }
      }]
    }),
  });
}

// ══════════════════════════════════════════════════════════
// TIKTOK EVENTS API
// ══════════════════════════════════════════════════════════
async function sendTiktokApi(p) {
  if (!CONFIG.tiktokPixelId || !CONFIG.server?.tiktok?.accessToken) return;

  // Mapeamento de nomes Meta → TikTok
  const eventMap = {
    Lead: 'SubmitForm', Purchase: 'CompletePayment',
    ViewContent: 'ViewContent', Contact: 'Contact',
    Schedule: 'Schedule', InitiateCheckout: 'InitiateCheckout',
    AddToCart: 'AddToCart', CompleteRegistration: 'CompleteRegistration',
    PageView: 'PageView',
  };

  const tiktokEvent = {
    event:     eventMap[p.event_name] || p.event_name,
    event_id:  p.event_id,                        // deduplicação com browser
    timestamp: new Date().toISOString(),           // obrigatório ISO 8601
    context: {
      ip:         p.ip,
      user_agent: p.userAgent,
      page: {
        url:      p.page_url,
        referrer: p.referrer || '',
      },
      user: {
        // SHA256 para TikTok
        // phone: normalizar (apenas dígitos, sem +) → sha256
        // email: lowercase + trim → sha256
        ...(p.email     && { email:        sha256(p.email.toLowerCase().trim()) }),
        ...(p.phone     && { phone_number: sha256(p.phone.replace(/\D/g, '')) }),
        // external_id: User ID first-party (_cdp_uid) → sha256 — Advanced Matching cross-session
        ...(p.userId    && { external_id:  sha256(p.userId) }),
        // cookies — enviar raw, sem hash
        ...(p.cookies.ttp    && { ttp:     p.cookies.ttp }),    // _ttp cookie → 'ttp' no payload
        ...(p.cookies.ttclid && { ttclid:  p.cookies.ttclid }), // TikTok click ID
      },
    },
    properties: {
      value:        p.value,
      currency:     p.currency,
      content_name: p.content_name,
      content_type: 'product',
    },
  };

  if (CONFIG.server.tiktok.testEventCode) {
    tiktokEvent.test_event_code = CONFIG.server.tiktok.testEventCode;
  }

  await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Access-Token': CONFIG.server.tiktok.accessToken,
    },
    body: JSON.stringify({
      pixel_code: CONFIG.tiktokPixelId,
      event:      tiktokEvent,
    }),
  });
}

---

### PASSO 6.4 — Campos SHA256 obrigatórios (padrão Meta e TikTok)

| Campo | Antes de hashear |
|---|---|
| `email` | lowercase + trim |
| `phone` | só dígitos (remover `+`, `-`, `(`, `)`, espaços) |
| `firstName` | lowercase + trim |
| `lastName` | lowercase + trim |
| `city` | lowercase + sem acentos + sem espaços + sem caracteres especiais (`normalizeCity()`) — ex: `'São Paulo'` → `'saopaulo'` |
| `state` | lowercase + trim (código ISO do estado, ex: `sp`) |
| `zipCode` | só dígitos |
| `country` | código ISO 2 letras lowercase (ex: `br`) |
| `_ttp` (TikTok) | **NÃO hashear** — enviar raw |
| `_fbp`, `_fbc` | **NÃO hashear** — enviar raw |
| `ctwa_clid` | **NÃO hashear** — enviar raw (lido da URL `?ctwa_clid=xxx`) |

---

### PASSO 6.5 — Como testar server-side

**Meta:**
1. Adicionar `test_event_code` nas credenciais (`CONFIG.server.metaCapi.testEventCode`)
2. Acessar Meta Events Manager → Testar Eventos → filtrar pelo test code

**TikTok:**
1. Adicionar `test_event_code` nas credenciais TikTok
2. Acessar TikTok Events Manager → Testar Eventos

**GA4 Measurement Protocol:**
Trocar URL de coleta para:
`https://www.google-analytics.com/debug/mp/collect?measurement_id=...&api_secret=...`
Retorna JSON com `validationMessages` indicando erros.

---

### Checklist server-side

- [ ] `ctwa_clid` capturado de `URLSearchParams` no carregamento da página e enviado no POST ao endpoint — **não hashear, não colocar no cookie**
- [ ] `ctwa_clid` enviado em `user_data.ctwa_clid` na Meta CAPI — permite atribuição a anúncios Click-to-WhatsApp
- [ ] `event_id` é o MESMO no browser e no servidor para deduplicação correta
- [ ] `action_source: 'website'` + `event_source_url` + `client_ip_address` + `client_user_agent` — todos obrigatórios na Meta CAPI
- [ ] Cookies `_fbp` e `_fbc` coletados no browser e repassados no POST
- [ ] Cookie `_ttp` coletado e repassado para TikTok (sem hash)
- [ ] `_ga` cookie extraído e `client_id` parseado corretamente para GA4 MP
- [ ] SHA256 aplicado em todos os campos de user_data — Meta e TikTok
- [ ] Telefone normalizado (só dígitos) antes do SHA256
- [ ] Email em lowercase antes do SHA256
- [ ] `sendServerEvent` no browser é fire-and-forget (sem await que bloqueie UX)
- [ ] Em produção: remover `testEventCode` das configurações

---

---

### Arquitetura de Servidor (Cloudflare Workers)

O CDP Edge opera exclusivamente sobre a infraestrutura **Cloudflare Workers (Quantum Tier)**.

O CDP Edge recomenda o uso de **Cloudflare Workers (Quantum Tier)** como endpoint de rastreamento principal. Ele oferece latência global de ~10ms, banco de dados D1 nativo e custo zero para a maioria dos funis ($0 para 100k req/dia).

| Recurso | Cloudflare Workers | Benefício Quantum Tier |
|---|---|---|
| Custo | Grátis / $5 pago | Zero barreira de entrada |
| Banco de Dados | D1 SQLite ✅ | Persistência de leads e match |
| SHA256 | WebCrypto Native | Segurança Enterprise |
| Resiliência | Edge Global | Escala infinita |

---

## PASSO 6.6 — Cloudflare Workers + D1 (Full Core Tracking)

> **Quando usar:** O padrão ouro do CDP Edge. Roda o endpoint `/track` no Cloudflare Workers e salva todos os eventos e perfis no banco D1 para atribuição precisa.

### Vantagens da Infraestrutura Cloudflare Native

| Recurso | Workers (free) |
|---|---|
| Custo | Grátis até 100k req/dia |
| SHA256 | WebCrypto API (nativa) |
| IP do usuário | Header `CF-Connecting-IP` (automático) |
| Banco de dados | D1 SQLite (grátis até 5M reads/dia) |
| Deploy | `wrangler deploy` (30s) |
| Manutenção | Zero |
| SSL | Automático |

> ⚠️ **Diferença crítica de código:** Workers usa **WebCrypto API** (assíncrona) para SHA256 em vez do módulo `crypto` do Node. Tudo mais é quase idêntico — `fetch()` é nativo, `Request`/`Response` são padrão Web API.

---

### 1. Estrutura do projeto Workers

```
cdp-edge-worker/
├── src/
│   └── worker.js          ← endpoint principal
├── wrangler.toml           ← configuração Cloudflare
└── schema.sql              ← schema do banco D1
```

---

### 2. `wrangler.toml` — configuração completa

```toml
name = "cdp-edge-tracking"
main = "src/worker.js"
compatibility_date = "2024-01-01"

# ── D1 Database ──────────────────────────────────────────────
[[d1_databases]]
binding = "DB"
database_name = "cdp-edge"
database_id = "COLE_AQUI_O_ID_DO_D1"   # gerado após: wrangler d1 create cdp-edge

# ── Variáveis públicas (não sensíveis) ───────────────────────
[vars]
META_PIXEL_ID    = "SEU_META_PIXEL_ID"
GA4_ID           = "G-XXXXXXXXXX"
TIKTOK_PIXEL_ID  = "SEU_TIKTOK_PIXEL_ID"

# Secrets (nunca colocar aqui — usar: wrangler secret put NOME):
# META_ACCESS_TOKEN
# GA4_API_SECRET
# TIKTOK_ACCESS_TOKEN
```

**Criar D1 e adicionar ID ao wrangler.toml:**
```bash
wrangler d1 create cdp-edge
# Saída: database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Copiar esse ID para o wrangler.toml
```

wrangler secret put META_ACCESS_TOKEN
wrangler secret put TIKTOK_ACCESS_TOKEN
wrangler secret put GA4_API_SECRET

---

### 3. `schema.sql` — banco D1 para leads

```sql
CREATE TABLE IF NOT EXISTS leads (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  event_id     TEXT NOT NULL,
  event_name   TEXT NOT NULL,
  email        TEXT DEFAULT '',
  phone        TEXT DEFAULT '',
  first_name   TEXT DEFAULT '',
  last_name    TEXT DEFAULT '',
  city         TEXT DEFAULT '',
  value        REAL DEFAULT 0,
  currency     TEXT DEFAULT 'BRL',
  utm_source   TEXT DEFAULT '',
  utm_medium   TEXT DEFAULT '',
  utm_campaign TEXT DEFAULT '',
  utm_content  TEXT DEFAULT '',
  utm_term     TEXT DEFAULT '',
  page_url     TEXT DEFAULT '',
  user_id      TEXT DEFAULT '',
  ip           TEXT DEFAULT '',
  created_at   TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_leads_email      ON leads(email);
CREATE INDEX IF NOT EXISTS idx_leads_created_at ON leads(created_at);
CREATE INDEX IF NOT EXISTS idx_leads_event_name ON leads(event_name);

-- ── Perfis de usuário (enriquecimento de webhooks) ──────────
-- Salva cookies do browser quando o usuário está no site.
-- Permite recuperar fbp/fbc/gclid ao receber webhooks externos (Hotmart, Kiwify etc.)
CREATE TABLE IF NOT EXISTS user_profiles (
  email       TEXT PRIMARY KEY,
  fbp         TEXT DEFAULT '',
  fbc         TEXT DEFAULT '',
  ttp         TEXT DEFAULT '',
  gclid       TEXT DEFAULT '',
  user_id     TEXT DEFAULT '',
  city        TEXT DEFAULT '',
  state       TEXT DEFAULT '',
  country     TEXT DEFAULT 'br',
  user_agent  TEXT DEFAULT '',
  ga_client   TEXT DEFAULT '',
  updated_at  TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON user_profiles(user_id);
```

**Criar as tabelas:**
```bash
wrangler d1 execute cdp-edge --file=schema.sql
```

---

### 4. `src/worker.js` — endpoint completo para Cloudflare Workers

```js
// ============================================================
// Recebe eventos do browser → Meta CAPI + TikTok Events API + D1 Database
// ============================================================

// SHA256 com WebCrypto API (nativa no Workers — não usar node:crypto)
const sha256 = async (val) => {
  if (!val) return '';
  const data   = new TextEncoder().encode(String(val).toLowerCase().trim());
  const buf    = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
};

// SHA256 SEM lowercase (para campos que já chegam normalizados)
const sha256Raw = async (val) => {
  if (!val) return '';
  const data = new TextEncoder().encode(String(val));
  const buf  = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
};

// Normaliza cidade: 'São Paulo' → 'saopaulo'
const normalizeCity = (city = '') =>
  String(city).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');

// IP real — Cloudflare injeta CF-Connecting-IP automaticamente
const getRealIp = (request) =>
  request.headers.get('CF-Connecting-IP') ||
  request.headers.get('X-Forwarded-For')?.split(',')[0].trim() || '';

// ── HANDLER PRINCIPAL ────────────────────────────────────────
export default {
  async fetch(request, env, ctx) {
    // CORS para o site poder chamar este Worker
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Origin':  '*',
          'Access-Control-Allow-Methods': 'POST, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type',
        },
      });
    }

    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    const body = await request.json().catch(() => ({}));

    const {
      event_name, event_id, page_url, referrer,
      cookies  = {},
      utms     = {},
      email    = '', phone     = '', firstName = '', lastName = '',
      city     = '', state     = '', zipCode   = '', country  = 'br',
      value    = 0,  currency  = 'BRL', content_name = '',
      content_ids = [], content_type = 'product',
      method   = '',
    } = body;

    const ip        = getRealIp(request);
    const userAgent = request.headers.get('User-Agent') || '';
    const eventTime = Math.floor(Date.now() / 1000);

    const { utm_source = '', utm_medium = '', utm_campaign = '',
            utm_content = '', utm_term = '' } = utms;

    // ── Geo: browser tem prioridade (GPS real via getCidade())
    // request.cf.country disponível grátis em todos os planos
    // request.cf.city/region só no plano Business ($200/mês) — não usar no free/paid
    const geoCountry = country || request.cf?.country?.toLowerCase() || 'br';
    // Fallback por IP somente se browser não enviou cidade (usuário negou geolocalização)
    let geoCity  = city;
    let geoState = state;
    if (!geoCity && ip && ip !== '127.0.0.1') {
      try {
        const geoRes  = await fetch(`https://ipapi.co/${ip}/json/`, { headers: { 'User-Agent': 'CDP Edge/1.0' } });
        const geoData = await geoRes.json();
        geoCity  = geoData.city         || '';
        geoState = geoData.region_code?.toLowerCase() || '';
      } catch { /* silencia falha no lookup */ }
    }

    // Disparar tudo em paralelo (não bloquear a resposta)
    const tasks = [
      sendMetaCapi({ event_name, event_id, page_url, referrer,
                     ip, userAgent, cookies, email, phone, firstName, lastName,
                     city: geoCity, state: geoState, zipCode, country: geoCountry,
                     value, currency, content_name, content_ids, content_type,
                     ctwaClid: cookies.ctwaClid || '',
                     userId:   cookies.userId   || '',
                     eventTime }, env),

      sendTiktokApi({ event_name, event_id, page_url, referrer,
                      ip, userAgent, cookies, email, phone,
                      userId: cookies.userId || '',
                      value, currency, content_name, eventTime }, env),

      sendGA4Mp({ event_name, event_id, page_url,
                  value, currency, content_name,
                  ga_client_id:  cookies.gaClient     || '',
                  ga_session_id: cookies.gaSessionId  || '',
                  ga_session_num:cookies.gaSessionNum || '',
                  eventTime }, env),
    ];

    // D1: salvar lead (fire-and-forget — não bloqueia resposta)
    if (env.DB && event_name) {
      tasks.push(
        env.DB.prepare(
          `INSERT INTO leads
           (event_id, event_name, email, phone, first_name, last_name, city,
            value, currency, utm_source, utm_medium, utm_campaign,
            utm_content, utm_term, page_url, user_id, ip, created_at)
           VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
        ).bind(
          event_id, event_name, email, phone, firstName, lastName, city,
          value, currency, utm_source, utm_medium, utm_campaign,
          utm_content, utm_term, page_url,
          cookies.userId || '', ip, new Date().toISOString()
        ).run().catch(() => {})
      );
    }

    // D1: salvar/enriquecer perfil do usuário (UPSERT preservando campos não-vazios)
    // Garante que fbp/fbc/ga_session salvos aqui podem ser recuperados em webhooks futuros (Hotmart etc.)
    if (env.DB && email) {
      tasks.push(
        env.DB.prepare(`
          INSERT INTO user_profiles
            (email, fbp, fbc, ttp, gclid, user_id, city, state, country, user_agent,
             ga_client, ga_session_id, ga_session_num, page_location, timestamp_ms, updated_at)
          VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
          ON CONFLICT(email) DO UPDATE SET
            fbp           = CASE WHEN excluded.fbp           != '' THEN excluded.fbp           ELSE fbp           END,
            fbc           = CASE WHEN excluded.fbc           != '' THEN excluded.fbc           ELSE fbc           END,
            ttp           = CASE WHEN excluded.ttp           != '' THEN excluded.ttp           ELSE ttp           END,
            gclid         = CASE WHEN excluded.gclid         != '' THEN excluded.gclid         ELSE gclid         END,
            user_id       = CASE WHEN excluded.user_id       != '' THEN excluded.user_id       ELSE user_id       END,
            city          = CASE WHEN excluded.city          != '' THEN excluded.city          ELSE city          END,
            state         = CASE WHEN excluded.state         != '' THEN excluded.state         ELSE state         END,
            ga_client     = CASE WHEN excluded.ga_client     != '' THEN excluded.ga_client     ELSE ga_client     END,
            ga_session_id = CASE WHEN excluded.ga_session_id != '' THEN excluded.ga_session_id ELSE ga_session_id END,
            ga_session_num= CASE WHEN excluded.ga_session_num!= '' THEN excluded.ga_session_num ELSE ga_session_num END,
            page_location = excluded.page_location,
            timestamp_ms  = excluded.timestamp_ms,
            updated_at    = excluded.updated_at
        `).bind(
          email,
          cookies.fbp || '', cookies.fbc || '', cookies.ttp || '',
          cookies.gclid || '', cookies.userId || '',
          geoCity || '', geoState || '', geoCountry || 'br',
          userAgent,
          cookies.gaClient     || '',
          cookies.gaSessionId  || '',
          cookies.gaSessionNum || '',
          page_url || '',
          String(Date.now()),
          new Date().toISOString()
        ).run().catch(() => {})
      );
    }

    // ctx.waitUntil: permite que as tasks terminem após a resposta ser enviada
    ctx.waitUntil(Promise.allSettled(tasks));

    return Response.json({ ok: true }, {
      headers: { 'Access-Control-Allow-Origin': '*' },
    });
  },
};

// ══════════════════════════════════════════════════════════
// META CONVERSIONS API
// ══════════════════════════════════════════════════════════
async function sendMetaCapi(p, env) {
  if (!env.META_PIXEL_ID || !env.META_ACCESS_TOKEN) return;

  const user_data = {
    client_ip_address: p.ip,
    client_user_agent: p.userAgent,
    ...(p.cookies.fbp  && { fbp:         p.cookies.fbp }),
    ...(p.cookies.fbc  && { fbc:         p.cookies.fbc }),
    ...(p.ctwaClid     && { ctwa_clid:   p.ctwaClid }),
    ...(p.userId       && { external_id: await sha256Raw(p.userId) }),
    ...(p.email        && { em:          await sha256(p.email) }),
    ...(p.phone        && { ph:          await sha256Raw(p.phone.replace(/\D/g, '')) }),
    ...(p.firstName    && { fn:          await sha256(p.firstName) }),
    ...(p.lastName     && { ln:          await sha256(p.lastName) }),
    ...(p.city         && { ct:          await sha256Raw(normalizeCity(p.city)) }),
    ...(p.state        && { st:          await sha256(p.state) }),
    ...(p.zipCode      && { zp:          await sha256Raw(p.zipCode.replace(/\D/g, '')) }),
    ...(p.country      && { country:     await sha256(p.country) }),
  };

  const serverEvent = {
    event_name:       p.event_name,
    event_time:       p.eventTime,
    event_id:         p.event_id,
    action_source:    'website',
    event_source_url: p.page_url,
    user_data,
    custom_data: {
      value:        p.value,
      currency:     p.currency,
      content_name: p.content_name,
      content_ids:  p.content_ids,
      content_type: p.content_type,
    },
  };

  if (env.META_TEST_EVENT_CODE) serverEvent.test_event_code = env.META_TEST_EVENT_CODE;

  await fetch(
    `https://graph.facebook.com/v25.0/${env.META_PIXEL_ID}/events?access_token=${env.META_ACCESS_TOKEN}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ data: [serverEvent] }),
    }
  );
}

// ══════════════════════════════════════════════════════════
// GA4 MEASUREMENT PROTOCOL (somente purchase)
// ══════════════════════════════════════════════════════════
async function sendGA4Mp(p, env) {
  if (!env.GA4_ID || !env.GA4_API_SECRET) return;
  if (p.event_name !== 'purchase') return;

  // client_id vem do cookie _ga (sem prefixo GA1.1.)
  const clientId = p.ga_client_id
    ? p.ga_client_id.replace(/^GA\d+\.\d+\./, '')
    : `${Date.now()}.${Math.floor(Math.random() * 1e9)}`;

  // session_id e session_number vêm do cookie _ga_PROPERTY (sem prefixo GS1.1.)
  const sessionId  = p.ga_session_id  || '';
  const sessionNum = p.ga_session_num || '1';

  const payload = {
    client_id: clientId,
    // timestamp_micros: vincula o evento à sessão correta (obrigatório para MP)
    ...(p.eventTime && { timestamp_micros: String(p.eventTime * 1_000_000) }),
    events: [{
      name: 'purchase',
      params: {
        transaction_id:       p.event_id,
        value:                p.value,
        currency:             p.currency,
        engagement_time_msec: 100,
        // session_id e session_number conectam ao relatório de GA4 correto
        ...(sessionId  && { session_id:     sessionId }),
        ...(sessionNum && { session_number: Number(sessionNum) }),
      },
    }],
  };

  await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=${env.GA4_ID}&api_secret=${env.GA4_API_SECRET}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    }
  );
}

// ══════════════════════════════════════════════════════════
// TIKTOK EVENTS API
// ══════════════════════════════════════════════════════════
async function sendTiktokApi(p, env) {
  if (!env.TIKTOK_PIXEL_ID || !env.TIKTOK_ACCESS_TOKEN) return;

  const eventMap = {
    Lead: 'SubmitForm', Purchase: 'CompletePayment',
    ViewContent: 'ViewContent', Contact: 'Contact',
    Schedule: 'Schedule', InitiateCheckout: 'InitiateCheckout',
    AddToCart: 'AddToCart', CompleteRegistration: 'CompleteRegistration',
    PageView: 'PageView',
  };

  const tiktokEvent = {
    event:     eventMap[p.event_name] || p.event_name,
    event_id:  p.event_id,
    timestamp: new Date().toISOString(),
    context: {
      ip:         p.ip,
      user_agent: p.userAgent,
      page:       { url: p.page_url, referrer: p.referrer || '' },
      user: {
        ...(p.email  && { email:        await sha256(p.email) }),
        ...(p.phone  && { phone_number: await sha256Raw(p.phone.replace(/\D/g, '')) }),
        ...(p.userId && { external_id:  await sha256Raw(p.userId) }),
        ...(p.cookies.ttp    && { ttp:    p.cookies.ttp }),
        ...(p.cookies.ttclid && { ttclid: p.cookies.ttclid }),
      },
    },
    properties: {
      value:        p.value,
      currency:     p.currency,
      content_name: p.content_name,
      content_type: 'product',
    },
  };

  if (env.TIKTOK_TEST_EVENT_CODE) tiktokEvent.test_event_code = env.TIKTOK_TEST_EVENT_CODE;

  await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Access-Token':  env.TIKTOK_ACCESS_TOKEN,
    },
    body: JSON.stringify({
      pixel_code: env.TIKTOK_PIXEL_ID,
      event:      tiktokEvent,
    }),
  });
}
```

---


---

### 6. Deploy do Worker

```bash
# Instalar Wrangler (CLI Cloudflare)
npm install -g wrangler

# Login (abre browser)
wrangler login

# Criar banco D1
wrangler d1 create cdp-edge
# Copiar o database_id gerado → colar em wrangler.toml

# Criar tabelas
wrangler d1 execute cdp-edge --file=schema.sql

# Adicionar secrets
wrangler secret put META_ACCESS_TOKEN
wrangler secret put TIKTOK_ACCESS_TOKEN
wrangler secret put GA4_API_SECRET

# Deploy
wrangler deploy

# URL gerada: https://cdp-edge-tracking.SEU_USUARIO.workers.dev
```

> Após o deploy, o Worker fica disponível em `https://cdp-edge-tracking.SEU_USUARIO.workers.dev`. Para usar `/track` no site, criar um **Custom Domain** nas configurações do Worker ou usar a URL completa.

---

### 7. Adaptar `sendServerEvent` no browser para apontar ao Worker

Se o site e o Worker estão em domínios diferentes, mudar a URL no `tracking.js`:

```js
// Em vez de '/track' (mesmo domínio — Next.js / Vite server)
// usar a URL do Worker:
const TRACKING_ENDPOINT = 'https://cdp-edge-tracking.SEU_USUARIO.workers.dev';

const sendServerEvent = (eventName, eventId, payload = {}) => {
  if (!isBrowser) return;
  fetch(TRACKING_ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      event_name: eventName,
      event_id:   eventId,
      page_url:   window.location.href,
      referrer:   document.referrer,
      cookies:    getCookies(),
      utms:       _utms,
      ...payload,
    }),
  }).catch(() => {});
};
```

> Se o site rodar em **Cloudflare Pages**, usar `functions/track.js` — o código do Worker é idêntico, só muda o handler para `export async function onRequestPost({ request, env, waitUntil })`.

---

### 8. Checklist deploy Cloudflare Workers

```
☐ wrangler.toml criado com name, main, d1_databases
☐ D1 criado: wrangler d1 create cdp-edge → ID copiado para wrangler.toml
☐ Schema criado: wrangler d1 execute cdp-edge --file=schema.sql
☐ Secrets adicionados via wrangler secret put
☐ wrangler deploy executado com sucesso
☐ URL do Worker testada com curl ou Postman
☐ Meta Events Manager → Testar Eventos → evento chegando
☐ TikTok Events Manager → evento chegando
☐ Em produção: remover META_TEST_EVENT_CODE e TIKTOK_TEST_EVENT_CODE
```

---

## PASSO 7 — CTWA SEM LANDING PAGE (Cloudflare Worker + WhatsApp Business API)

> **Quando usar:** usuário roda anúncios Click-to-WhatsApp que vão direto para o WhatsApp, sem passar por nenhuma página do site. Não existe pixel no browser — o rastreamento é 100% server-side via webhook.

### Arquitetura

```
Anúncio Meta (Instagram / Facebook)
    ↓ clique em "Enviar Mensagem"
WhatsApp abre com conversa iniciada
    ↓ usuário manda primeira mensagem
Meta Cloud API (webhook oficial)
    ↓ POST para Cloudflare Worker
Worker (edge, ~5ms)
    ├── Meta CAPI  (action_source: 'business_messaging')
    └── Cloudflare D1 (Identity Graph)
```

### Diferença crítica de action_source

| Cenário | action_source | ctwa_clid vem de |
|---|---|---|
| Botão WhatsApp no site | `website` | `?ctwa_clid=` na URL (JavaScript) |
| CTWA → site → WhatsApp | `website` | `?ctwa_clid=` na URL (JavaScript) |
| CTWA → WhatsApp direto | `business_messaging` | `message.referral.ctwa_clid` (webhook) |

### PASSO 7.1 — Cloudflare Worker completo

```js
// worker.js — Cloudflare Worker
// Deploy: wrangler deploy
// Variáveis de ambiente: META_PIXEL_ID, META_CAPI_TOKEN, VERIFY_TOKEN, SHEET_WEBHOOK_URL

const sha256 = async (val) => {
  const data = new TextEncoder().encode(String(val).toLowerCase().trim());
  const hash = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,'0')).join('');
};

export default {
  async fetch(request, env) {

    // ── Verificação do webhook (GET — Meta envia uma vez ao configurar) ──
    if (request.method === 'GET') {
      const url       = new URL(request.url);
      const mode      = url.searchParams.get('hub.mode');
      const token     = url.searchParams.get('hub.verify_token');
      const challenge = url.searchParams.get('hub.challenge');

      if (mode === 'subscribe' && token === env.VERIFY_TOKEN) {
        return new Response(challenge, { status: 200 });
      }
      return new Response('Forbidden', { status: 403 });
    }

    // ── Receber mensagem do WhatsApp (POST) ──
    if (request.method === 'POST') {
      const body    = await request.json();
      const change  = body.entry?.[0]?.changes?.[0]?.value;
      const message = change?.messages?.[0];

      // Responde 200 imediatamente — Meta exige resposta em < 20s
      if (!message || message.type === 'status') {
        return new Response('ok', { status: 200 });
      }

      // ── Extrair dados da conversa ──
      const phone     = message.from;                          // ex: '5511999999999'
      const name      = change?.contacts?.[0]?.profile?.name || '';
      const ctwaClid  = message.referral?.ctwa_clid  || '';   // só existe se veio de anúncio CTWA
      const adId      = message.referral?.source_id  || '';
      const adUrl     = message.referral?.source_url || '';
      const timestamp = Math.floor(Date.now() / 1000);
      const eventId   = `wa_${phone}_${timestamp}`;

      const lead = {
        phone,
        name,
        ctwa_clid:  ctwaClid,
        ad_id:      adId,
        ad_url:     adUrl,
        event_id:   eventId,
        timestamp:  new Date().toISOString(),
        source:     ctwaClid ? 'ctwa_ad' : 'organico',
      };

      // Rodar em paralelo sem bloquear a resposta ao WhatsApp
      // (usar waitUntil para não cancelar após o return)
      const ctx = { waitUntil: (p) => p }; // fallback se não tiver ExecutionContext
      await Promise.allSettled([
        sendMetaCapi(lead, env, await sha256(phone.replace(/\D/g, ''))),
      ]);

      return new Response('ok', { status: 200 });
    }

    return new Response('Method Not Allowed', { status: 405 });
  }
};

// ── META CAPI (action_source: business_messaging) ─────────
async function sendMetaCapi(lead, env, hashedPhone) {
  if (!env.META_PIXEL_ID || !env.META_CAPI_TOKEN) return;

  const user_data = {
    ph: hashedPhone,
    ...(lead.ctwa_clid && { ctwa_clid: lead.ctwa_clid }),  // não hashear
  };

  await fetch(
    `https://graph.facebook.com/v25.0/${env.META_PIXEL_ID}/events?access_token=${env.META_CAPI_TOKEN}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        data: [{
          event_name:        'Contact',
          event_time:        Math.floor(Date.now() / 1000),
          event_id:          lead.event_id,
          action_source:     'business_messaging',  // ← não é 'website'
          messaging_channel: 'whatsapp',            // ← obrigatório
          user_data,
        }]
      }),
    }
  );
}
```

### PASSO 7.3 — Variáveis de ambiente no Cloudflare

No painel: Workers & Pages → seu Worker → Settings → Variables and Secrets

```
META_PIXEL_ID       = 1234567890123456
META_CAPI_TOKEN     = seu_token_capi_do_events_manager
VERIFY_TOKEN        = qualquer_palavra_secreta (ex: cdp-edge2025)
SHEET_WEBHOOK_URL   = https://script.google.com/macros/s/SEU_ID/exec
```

### PASSO 7.4 — Conectar o Worker ao WhatsApp Business API

**Meta Cloud API (gratuita):**
1. [developers.facebook.com](https://developers.facebook.com) → App → WhatsApp → Configuração
2. Webhook URL: `https://seu-worker.workers.dev`
3. Verify Token: mesmo valor de `VERIFY_TOKEN`
4. Assinar campo: `messages`

**Meta Cloud API (única opção suportada):**
developers.facebook.com → App → WhatsApp → Configuração → Webhook URL: `https://seu-worker.workers.dev` → Assinar campo: `messages`

### Checklist CTWA sem landing page

- [ ] Worker responde `200 OK` imediatamente — Meta cancela webhook se demorar mais de 20s
- [ ] Verificar se `message.referral` existe antes de ler `ctwa_clid` — nem toda mensagem vem de anúncio
- [ ] `action_source: 'business_messaging'` + `messaging_channel: 'whatsapp'` — obrigatórios juntos
- [ ] `ctwa_clid` enviado raw em `user_data.ctwa_clid` — não hashear
- [ ] Telefone normalizado (só dígitos) antes do SHA256
- [ ] `event_id` único por mensagem para evitar duplicatas (ex: `wa_{phone}_{timestamp}`)
- [ ] Eventos chegando no Meta Events Manager
- [ ] `VERIFY_TOKEN` configurado no Worker antes de cadastrar o webhook na Meta
- [ ] Testar com Meta Events Manager → Testar Eventos usando `testEventCode`

---

## PASSO 8.4 — Pixel de Mensagens Meta (WhatsApp)

#### 1. Criar App no Meta Developers

1. Acessar https://developers.facebook.com
2. Login (se pedir conta: conectar com telefone; se der erro: adicionar cartão como método de pagamento)
3. Meus Apps → Criar App:

| Campo | Valor |
|---|---|
| Nome | qualquer nome descritivo |
| Caso de uso | Outro |
| Tipo | Business (Empresa) |
| Portfólio | selecionar o BM que contém a conta de anúncios |

#### 2. Configurar API de Marketing

API DE MARKETING → Configurar → Ferramentas → Obter token de acesso

Permissões obrigatórias:
- `ads_management`
- `ads_read`
- `read_insights`

Gerar token → **salvar imediatamente** (não é exibido novamente)

#### 3. Criar Pixel de Mensagens

No Gerenciador de Eventos:
1. Criar pixel de mensagens
2. Vincular a: uma Página do Facebook **ou** um perfil do Instagram
3. Se não aparecer páginas → desvincular a página de pixels antigos antes de tentar

#### 4. Criar Usuário do Sistema (BM)

URL: https://business.facebook.com/latest/settings/system_users

1. Usuários → Usuários do sistema → Adicionar
2. Definir nome, System user role = `admin`
3. Atribuir todos os ativos
4. Gerar token com expiração **nunca** → Copiar token

#### 5. Salvar token no Cloudflare Secrets

Use o Wrangler para persistir o token de forma segura no ambiente do Worker:

```bash
wrangler secret put META_MESSAGING_TOKEN
# Cole o token gerado no passo anterior
```

---

### PASSO 8.6 — Arquitetura Cloudflare Native (Quantum Tier) 🛡️⚓🚀

```
                     ┌─────────────────────────────────────┐
                     │         CLOUDFLARE ECOSYSTEM        │
                     │   Tudo num único Domínio Raiz       │
                     └──────────────┬──────────────────────┘
                                    │
                     ┌──────────────▼──────────────────────┐
                     │         Cloudflare Edge              │
                     │  (Workers + D1 + R2 + Queues)        │
                     │                                      │
                     │  ┌──────────┐  ┌──────────┐  ┌─────┐ │
                     │  │ Payments │  │ Messaging│  │ CRM │ │
                     │  │ (Ticto)  │  │ (Resend) │  │ (D1)│ │
                     │  └─────┬────┘  └─────┬────┘  └──┬──┘ │
                     │        │             │          │    │
                     │  ┌─────▼─────────────▼──────────▼──┐ │
                     │  │       Identity Graph (D1)       │ │
                     │  │   (FBP / FBC / GCLID / UTMs)    │ │
                     │  └─────────────────────────────────┘ │
                     └─────────────────────────────────────┘
                               │
               ┌───────────────┼──────────────────┐
               │               │                  │
    ┌──────────▼──┐   ┌────────▼────────┐   ┌─────▼──────────┐
    │  WhatsApp   │   │   Meta CAPI     │   │   A/B Edge     │
    │ (Cloud API) │   │     v25.0       │   │   Routing      │
    └─────────────┘   └─────────────────┘   └────────────────┘
```

**Fluxo de Dados Autônomo:**
1. **Inbound**: O Worker intercepta Webhooks (Ticto) ou Cliques no Site.
2. **Process**: IA Preditiva (LTV) e Fingerprinting cruzam dados no D1.
3. **Outbound**: Disparos simultâneos via Queues para Meta CAPI, Google Ads, TikTok e Mensageria (WhatsApp/Resend).
4. **Resgate**: UTMs perdidas são resgatadas via Identity Graph persistente no domínio.

---

### PASSO 8.7 — Excelência em Infraestrutura (Quantum Tier)

A arquitetura CDP Edge Quantum Tier é otimizada para máxima performance com redundância global, operando exclusivamente na borda (Edge).

---

**Cloudflare DNS:**
- [ ] Nameservers apontados para Cloudflare
- [ ] Proxy ☁️ ativado para o domínio principal (Segurança & Performance)
- [ ] Registros CNAME configurados para os subdomínios de tracking
- [ ] SSL configurado como "Full (Strict)"

---

## PASSO 2.21 — Webhooks Externos: Ticto (v2.0)

> Use esta seção para orientar o usuário na configuração da Ticto como gateway principal de conversões offline e ROI.

### Configuração na Ticto
1. Acesse **Tictools > Webhooks** no menu lateral esquerdo.
2. Clique em **+ Nova Pasta** (opcional para organização).
3. Clique em **+ Criar Webhook**.
4. **Endpoint URL:** `https://seu-worker.workers.dev/api/wh/ticto`
5. **Versão:** Selecione **2.0** (Recomendado).
6. **Formato:** Selecione **JSON**.
7. **Eventos Críticos (Mapeamento CDP Edge):**
   - `15. Venda Realizada` -> `Purchase`
   - `1. Abandono de Carrinho` -> `AddToCart` / CRM
   - `4. Pix Gerado` / `2. Boleto Impresso` -> `InitiateCheckout`
8. **Token:** Copie o Token gerado e configure no Worker via `wrangler secret put TICTO_TOKEN`.

### Validação Automática
A Ticto enviará um POST de teste imediatamente. O Worker **DEVE** retornar status `200 OK` para que a integração seja salva com sucesso.

### Payload de Referência (v2.0)
```json
{
  "event_id": 15,
  "event_name": "Venda Realizada",
  "customer": {
    "email": "cliente@email.com",
    "phone": "5511999999999"
  },
  "transaction": {
    "total_amount": 197.00,
    "currency": "BRL"
  }
}
```

---

## PASSO 2.22 — E-mail Transacional: Resend API

> Use esta seção para configurar o envio de e-mails automáticos (Venda, Abandono, Bônus) diretamente do Cloudflare Worker.

### Configuração no Resend
1. Acesse **Resend > Domains** e adicione seu domínio.
2. No Cloudflare, adicione os registros **MX, TXT (SPF/DKIM)** que o Resend fornecerá.
3. Gere uma **API Key** e salve via `wrangler secret put RESEND_API_KEY`.
4. Envie e-mails via `POST https://api.resend.com/emails`.

---

## PASSO 2.23 — Spotify Ads: Pixel + Conversions API (v1)

> O Spotify Ads é a peça final do Quantum Tier. Permite rastrear conversões originadas de áudio e display no Spotify.

### Especificações Técnicas
- **Endpoint**: `https://advertising-api.spotify.com/conversion/v1/accounts/{ACCOUNT_ID}/events`
- **Autenticação**: Bearer Token (`SPOTIFY_ACCESS_TOKEN`)
- **Deduplicação**: `event_id` obrigatório entre Browser e Server
- **PII Hashing**: SHA-256 obrigatório para E-mail e Telefone (Advanced Matching)

### Eventos Principais
| Evento | Conversão | Descrição |
|---|---|---|
| `ViewContent` | Milestone | Visualização de página de produto/serviço |
| `AddToCart` | Intenção | Adicionado ao carrinho |
| `Purchase` | Conversão | Compra realizada via Spotify Ads |
| `Lead` | Conversão | Captação de lead via áudio/display |

### Implementação de Borda (Worker)
Sempre use `ctx.waitUntil` para despachar o evento para o Spotify sem atrasar a resposta ao usuário.

---

### 2.3 Segurança e Hashing (WebCrypto API)
O hashing de PII (e-mail, telefone, nome) deve ser feito obrigatoriamente no servidor usando a API nativa do Cloudflare Workers:
```javascript
async function sha256(data) {
  const msgBuffer = new TextEncoder().encode(data.toLowerCase().trim());
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
  return Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}
```

---

## 📋 3. MAPEAMENTO DE APIs

| Plataforma | Versão | Endpoint Principal |
|---|---|---|
| **Meta (CAPI)** | v25.0 | `https://graph.facebook.com/v25.0/{PIXEL_ID}/events` |
| **TikTok (Events)** | v1.3 | `https://business-api.tiktok.com/open_api/v1.3/event/track/` |
| **Google (GA4)** | MP | `https://www.google-analytics.com/mp/collect` |
| **Spotify (Ads)** | v1 | `https://advertising-api.spotify.com/conversion/v1/accounts/{ACC_ID}/events` |

---

## 🔄 4. PROCESSO DE CONFIGURAÇÃO

1.  **Setup Worker**: Criar Worker no Cloudflare e vincular banco D1.
2.  **Schema D1**: Executar `schema.sql` para criar as tabelas de identidade e logs.
3.  **Wrangler Config**: Configurar `wrangler.toml` com os bindings e secrets (tokens de API).
4.  **SDK Site**: Inserir `cdpTrack.js` e `tracking.config.js` no site.
5.  **Event Mapping**: Configurar gatilhos de clique e formulário no site.
6.  **Webhooks**: Configurar o endpoint de webhook na plataforma de vendas (Ticto/Hotmart).

---

## 🤖 8. MELHORIA CONTÍNUA AUTOMÁTICA (FASE 5)

A Fase 5 fecha o ciclo de dados: cada evento coletado alimenta modelos e alertas que, automaticamente, melhoram a qualidade de atribuição na próxima semana — sem intervenção manual.

---

### 8.1 O Ciclo de Melhoria Contínua

```
Evento /track
    │
    ├─ match_quality_log (has_email, has_fbp, has_phone, has_fbc, was_email_recovered)
    │       │
    │       └─ Cron semanal analisa janela de 2h
    │               → email_rate < 40%  → alerta CallMeBot
    │               → fbp_rate < 30%    → alerta CallMeBot
    │               → composite_score < 45% → alerta CallMeBot
    │
    ├─ user_profiles (Identity Graph)
    │       │
    │       └─ Auto-Enrich: antes de cada dispatch Meta CAPI,
    │               Worker consulta user_profiles por userId
    │               → recupera email/fbp/fbc/phone ausentes
    │               → evento vai para Meta COM email → EMQ sobe
    │
    ├─ leads × purchases (D1)
    │       │
    │       └─ Cron semanal treina regressão logística
    │               → pesos gravados em ltv_model_weights (is_active=1)
    │               → cached em KV → próximo /track usa modelo treinado
    │               → fallback heurístico se modelo não disponível
    │
    └─ user_profiles (alta intenção) → Customer Match Export semanal
            → GET /export/customer-match → CSV para Google Ads
            → Meta Custom Audience auto-atualizada
```

---

### 8.2 Match Quality — Monitoramento Automático

Cada dispatch para a Meta CAPI registra na tabela `match_quality_log`:

| Campo | O que indica |
|---|---|
| `has_email` | Evento foi com email (principal fator de EMQ) |
| `has_phone` | Evento foi com telefone |
| `has_fbp` | Cookie _fbp presente (atribuição via pixel) |
| `has_fbc` | Cookie _fbc presente (atribuição via clique em anúncio) |
| `was_email_recovered` | Email recuperado via Identity Graph (Auto-Enrich) |

**Thresholds de alerta (cron semanal):**

| Métrica | Threshold de alerta | Impacto |
|---|---|---|
| `email_rate` | < 40% | Atribuição fraca — muitos eventos sem email |
| `fbp_rate` | < 30% | Pixel sem rastrear adequadamente |
| `composite_score` | < 45% | EMQ geral degradado |

A view `v_match_quality_24h` agrega os dados prontos para consulta instantânea:

```sql
SELECT * FROM v_match_quality_24h;
-- Retorna: event_count, email_rate, phone_rate, fbp_rate, fbc_rate, composite_score
```

---

### 8.3 Identity Graph — Auto-Enrich Antes do Dispatch

Antes de cada dispatch para a Meta CAPI, o Worker consulta `user_profiles` usando o `userId` do evento. Se o evento chegou sem email ou sem fbp, mas o perfil do usuário tem esses dados de uma sessão anterior, eles são injetados automaticamente no payload.

**Resultado prático:**
- Lead preencheu formulário na semana passada (email registrado no perfil)
- Hoje clicou em anúncio e disparou `ViewContent` sem email
- Worker recupera o email do perfil → evento vai para Meta COM email
- Meta recebe o sinal com Advanced Matching completo → melhor atribuição
- `was_email_recovered = 1` fica registrado em `match_quality_log`

Esse processo é transparente: o browser não precisa enviar nada a mais.

---

### 8.4 LTV Real — Modelo Treinado em Dados Reais

**Como funciona:**

1. Cron semanal executa no Worker
2. Busca leads com `purchase = true/false` dos últimos 90 dias no D1
3. Treina regressão logística com as features disponíveis (ltv_score, behavior_score, engagement_score, utm_source, state)
4. Grava os pesos em `ltv_model_weights` com `is_active = 1`
5. Cacheia os pesos em KV para acesso em ~0ms

**Uso em runtime (cada `/track`):**
- Worker verifica se há pesos ativos no KV
- Se sim: usa o modelo treinado para prever LTV
- Se não: fallback para heurística baseada em regras
- Score 0-100 → classificação High/Medium/Low → valor estimado em BRL

**Tabela `ltv_model_weights`:**

| Campo | Descrição |
|---|---|
| `trained_at` | Timestamp do treinamento |
| `is_active` | Apenas 1 registro ativo por vez |
| `accuracy` | Acurácia do modelo no conjunto de teste (0-1) |
| `weights_json` | Pesos serializados da regressão logística |

---

### 8.5 A/B LTV — Auto-Winner

Quando um experimento A/B de prompt LTV tem uma variação com acurácia ≥5pp acima do controle:

1. O Worker declara o vencedor automaticamente via `POST /api/ltv/ab-test/winner`
2. O prompt vencedor é ativado para todos os novos eventos
3. Um alerta WhatsApp é enviado informando qual variação ganhou e a diferença de acurácia

Isso elimina a necessidade de revisão manual de experimentos.

---

### 8.6 Customer Match — Sync Semanal

O cron semanal também exporta os perfis com `intention_level = 'high'` via `GET /export/customer-match`.

- Formato CSV compatível com Google Customer Match
- Emails e telefones hashados (SHA-256) conforme exigido pelo Google
- Pode ser automatizado para upload direto via Google Ads API

---

### 8.7 Como Cada Tabela Alimenta o Desempenho

| Tabela | Alimenta | Resultado |
|---|---|---|
| `match_quality_log` | Alerta de degradação de EMQ | Fix proativo antes do CPA subir |
| `user_profiles` | Auto-Enrich no dispatch | Mais eventos com email → EMQ sobe |
| `ltv_model_weights` | Score LTV real no /track | Bids mais inteligentes via bid_recommendations |
| `ml_segments` + `bid_recommendations` | Bid por segmento × plataforma | ROAS por cluster |
| `fraud_signals` | Tráfego limpo | Melhor atribuição Meta/Google |
| `ltv_ab_assignments` | Experimentos de prompt | Prompt com maior acurácia vence automaticamente |

---

### 8.8 Endpoints para Consultar os Dados

| Endpoint | O que retorna |
|---|---|
| `GET /api/fraud/stats` | Dashboard de fraude nas últimas 24h |
| `GET /api/segmentation/list` | Segmentos ML ativos com métricas |
| `GET /api/bidding/status` | Recomendações de bid por segmento × plataforma |
| `GET /api/ltv/ab-test/results` | Acurácia por variação + vencedor recomendado |
| `GET /export/customer-match` | Export de leads high-intent para Google Ads |

---

### 8.9 Sequência Completa de Migrations D1 (incluindo Fase 5)

```bash
wrangler d1 execute cdp-edge-db --file=schema.sql --remote
wrangler d1 execute cdp-edge-db --file=migrate-v6.sql --remote
wrangler d1 execute cdp-edge-db --file=schema-segmentation.sql --remote
wrangler d1 execute cdp-edge-db --file=schema-bidding.sql --remote
wrangler d1 execute cdp-edge-db --file=schema-ab-ltv.sql --remote
wrangler d1 execute cdp-edge-db --file=schema-fraud.sql --remote
wrangler d1 execute cdp-edge-db --file=schema-indexes.sql --remote   # Índices compostos de performance
wrangler d1 execute cdp-edge-db --file=migrate-v7.sql --remote       # Fase 5: ltv_model_weights + match_quality_log
```
