UNPKG

@jussimirvfx/meta-pixel-tracking

Version:

Sistema completo de tracking do Meta Pixel (Pixel + CAPI) com proteção anti-adblock para landing pages

803 lines (669 loc) 22.9 kB
#!/usr/bin/env node /** * Setup automático para Meta Pixel/CAPI * Cria API routes, servidor de desenvolvimento e configuração completa */ const fs = require('fs'); const path = require('path'); // Cores para console const colors = { green: '\x1b[32m', blue: '\x1b[34m', yellow: '\x1b[33m', red: '\x1b[31m', reset: '\x1b[0m', bold: '\x1b[1m' }; function log(message, color = 'blue') { console.log(`${colors[color]}${message}${colors.reset}`); } function logSuccess(message) { log(`✅ ${message}`, 'green'); } function logInfo(message) { log(`ℹ️ ${message}`, 'blue'); } function logWarning(message) { log(`⚠️ ${message}`, 'yellow'); } function logError(message) { log(`❌ ${message}`, 'red'); } // Verificar se estamos no diretório raiz do projeto function checkProjectStructure() { const requiredFiles = ['package.json', 'vite.config.js', 'vite.config.ts']; const missingFiles = requiredFiles.filter(file => !fs.existsSync(file)); if (missingFiles.length > 0) { logError(`Arquivos necessários não encontrados: ${missingFiles.join(', ')}`); logError('Execute este script no diretório raiz do seu projeto Vite'); process.exit(1); } logSuccess('Estrutura do projeto verificada'); } // Criar diretório api se não existir function createApiDirectory() { const apiDir = path.join(process.cwd(), 'api'); if (!fs.existsSync(apiDir)) { fs.mkdirSync(apiDir, { recursive: true }); logSuccess('Diretório api/ criado'); } else { logInfo('Diretório api/ já existe'); } return apiDir; } // Template para API route de captura de IP function getIpApiTemplate() { return `// API Route para captura de IP real // Compatível com Vercel Functions e servidor Express export default function handler(req, res) { // Configurar CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); // Responder a requisições OPTIONS (preflight) if (req.method === 'OPTIONS') { return res.status(200).end(); } // Capturar IP real do cliente const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip || 'unknown'; // Log para debug console.log('[IP API] IP capturado:', { clientIP, headers: { 'x-forwarded-for': req.headers['x-forwarded-for'], 'x-real-ip': req.headers['x-real-ip'], 'cf-connecting-ip': req.headers['cf-connecting-ip'] } }); return res.status(200).json({ success: true, ip: clientIP, timestamp: Date.now() }); } `; } // Template para API route do Meta Conversions function getMetaConversionsTemplate() { return `// API Route para Meta Pixel Conversions // Compatível com Vercel Functions e servidor Express // Configuração do Meta Pixel const META_API_ACCESS_TOKEN = process.env.VITE_META_API_ACCESS_TOKEN || process.env.META_API_ACCESS_TOKEN; const META_PIXEL_ID = process.env.VITE_META_PIXEL_ID || process.env.META_PIXEL_ID; const META_TEST_EVENT_CODE = process.env.VITE_META_TEST_EVENT_CODE || process.env.META_TEST_EVENT_CODE; export default async function handler(req, res) { // Configurar CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); // Responder a requisições OPTIONS (preflight) if (req.method === 'OPTIONS') { return res.status(200).end(); } // Health check if (req.method === 'GET') { return res.status(200).json({ success: true, message: 'Meta Pixel Conversions API', configured: { hasPixelId: !!META_PIXEL_ID, hasAccessToken: !!META_API_ACCESS_TOKEN, hasTestEventCode: !!META_TEST_EVENT_CODE } }); } try { // Verificar configuração if (!META_API_ACCESS_TOKEN || !META_PIXEL_ID) { console.error('Configuração do Meta Pixel incompleta'); return res.status(500).json({ success: false, error: 'Configuração incompleta' }); } // Parse do body const eventData = req.body; // Validar dados do evento if (!eventData || !eventData.event_name || !eventData.event_time || !eventData.event_id) { return res.status(400).json({ success: false, error: 'Dados do evento inválidos' }); } // Capturar IP real do cliente const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip || 'unknown'; // Preparar dados do usuário com IP real const userData = { // Formato correto do Meta - arrays para dados hasheados em: eventData.user_data?.em ? [eventData.user_data.em] : undefined, ph: eventData.user_data?.ph ? [eventData.user_data.ph] : undefined, fn: eventData.user_data?.fn ? [eventData.user_data.fn] : undefined, ln: eventData.user_data?.ln ? [eventData.user_data.ln] : undefined, // Dados não hasheados fbc: eventData.user_data?.fbc || undefined, fbp: eventData.user_data?.fbp || undefined, client_ip_address: clientIP, // IP real capturado client_user_agent: eventData.user_data?.client_user_agent || req.headers['user-agent'] || 'unknown', // Dados geográficos ct: eventData.user_data?.ct || undefined, st: eventData.user_data?.st || undefined, zp: eventData.user_data?.zp || undefined, country: eventData.user_data?.country || undefined, // External ID external_id: eventData.user_data?.external_id || undefined }; // Construir payload para Meta const payload = { data: [{ event_name: eventData.event_name, event_time: eventData.event_time, event_id: eventData.event_id, event_source_url: eventData.event_source_url, action_source: eventData.action_source || 'website', user_data: userData, custom_data: eventData.custom_data || {} }] }; // Adicionar test_event_code se estiver em modo de teste if (META_TEST_EVENT_CODE) { payload.test_event_code = META_TEST_EVENT_CODE; } // Enviar para Meta Conversions API (v23) const metaResponse = await fetch( \`https://graph.facebook.com/v23.0/\${META_PIXEL_ID}/events?access_token=\${META_API_ACCESS_TOKEN}\`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload) } ); const result = await metaResponse.json(); if (!metaResponse.ok) { console.error('Erro na API do Meta:', result); return res.status(metaResponse.status).json({ success: false, error: result.error?.message || 'Erro ao enviar para Meta', metaResponse: result }); } // Log de sucesso console.log('✅ Evento enviado para Meta:', { event: eventData.event_name, eventId: eventData.event_id, clientIP, success: true }); return res.status(200).json({ success: true, result, clientIP, eventId: eventData.event_id }); } catch (error) { console.error('Erro ao processar evento:', error); return res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Erro interno' }); } } `; } // Template para servidor Express completo function getDevServerTemplate() { return `// Servidor Express para desenvolvimento local // Integra Vite e API routes do Meta Pixel const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware para parsing de JSON app.use(express.json()); // Configurar CORS app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.status(200).end(); } next(); }); // API Route para captura de IP app.get('/api/get-ip', (req, res) => { const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip || 'unknown'; console.log('[IP API] IP capturado:', clientIP); res.json({ success: true, ip: clientIP, timestamp: Date.now() }); }); // API Route para Meta Conversions app.post('/api/meta/conversions', async (req, res) => { try { // Configuração do Meta Pixel const META_API_ACCESS_TOKEN = process.env.VITE_META_API_ACCESS_TOKEN || process.env.META_API_ACCESS_TOKEN; const META_PIXEL_ID = process.env.VITE_META_PIXEL_ID || process.env.META_PIXEL_ID; const META_TEST_EVENT_CODE = process.env.VITE_META_TEST_EVENT_CODE || process.env.META_TEST_EVENT_CODE; // Verificar configuração if (!META_API_ACCESS_TOKEN || !META_PIXEL_ID) { console.error('Configuração do Meta Pixel incompleta'); return res.status(500).json({ success: false, error: 'Configuração incompleta' }); } const eventData = req.body; // Validar dados do evento if (!eventData || !eventData.event_name || !eventData.event_time || !eventData.event_id) { return res.status(400).json({ success: false, error: 'Dados do evento inválidos' }); } // Capturar IP real do cliente const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip || 'unknown'; // Preparar dados do usuário com IP real const userData = { em: eventData.user_data?.em ? [eventData.user_data.em] : undefined, ph: eventData.user_data?.ph ? [eventData.user_data.ph] : undefined, fn: eventData.user_data?.fn ? [eventData.user_data.fn] : undefined, ln: eventData.user_data?.ln ? [eventData.user_data.ln] : undefined, fbc: eventData.user_data?.fbc || undefined, fbp: eventData.user_data?.fbp || undefined, client_ip_address: clientIP, client_user_agent: eventData.user_data?.client_user_agent || req.headers['user-agent'] || 'unknown', ct: eventData.user_data?.ct || undefined, st: eventData.user_data?.st || undefined, zp: eventData.user_data?.zp || undefined, country: eventData.user_data?.country || undefined, external_id: eventData.user_data?.external_id || undefined }; // Construir payload para Meta const payload = { data: [{ event_name: eventData.event_name, event_time: eventData.event_time, event_id: eventData.event_id, event_source_url: eventData.event_source_url, action_source: eventData.action_source || 'website', user_data: userData, custom_data: eventData.custom_data || {} }] }; if (META_TEST_EVENT_CODE) { payload.test_event_code = META_TEST_EVENT_CODE; } // Enviar para Meta Conversions API (v23) const metaResponse = await fetch( \`https://graph.facebook.com/v23.0/\${META_PIXEL_ID}/events?access_token=\${META_API_ACCESS_TOKEN}\`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload) } ); const result = await metaResponse.json(); if (!metaResponse.ok) { console.error('Erro na API do Meta:', result); return res.status(metaResponse.status).json({ success: false, error: result.error?.message || 'Erro ao enviar para Meta', metaResponse: result }); } console.log('✅ Evento enviado para Meta:', { event: eventData.event_name, eventId: eventData.event_id, clientIP, success: true }); return res.status(200).json({ success: true, result, clientIP, eventId: eventData.event_id }); } catch (error) { console.error('Erro ao processar evento:', error); return res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Erro interno' }); } }); // Health check para Meta Conversions app.get('/api/meta/conversions', (req, res) => { const META_PIXEL_ID = process.env.VITE_META_PIXEL_ID || process.env.META_PIXEL_ID; const META_API_ACCESS_TOKEN = process.env.VITE_META_API_ACCESS_TOKEN || process.env.META_API_ACCESS_TOKEN; const META_TEST_EVENT_CODE = process.env.VITE_META_TEST_EVENT_CODE || process.env.META_TEST_EVENT_CODE; res.json({ success: true, message: 'Meta Pixel Conversions API', configured: { hasPixelId: !!META_PIXEL_ID, hasAccessToken: !!META_API_ACCESS_TOKEN, hasTestEventCode: !!META_TEST_EVENT_CODE } }); }); // Proxy para Vite em desenvolvimento if (process.env.NODE_ENV !== 'production') { app.use('/', createProxyMiddleware({ target: 'http://localhost:5173', // Porta padrão do Vite changeOrigin: true, ws: true, // WebSocket para HMR logLevel: 'silent' })); } // Servir arquivos estáticos em produção if (process.env.NODE_ENV === 'production') { app.use(express.static(path.join(__dirname, 'dist'))); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); } app.listen(PORT, () => { console.log(\`🚀 Servidor rodando em http://localhost:\${PORT}\`); console.log(\`📡 API Routes disponíveis:\`); console.log(\` GET /api/get-ip\`); console.log(\` GET /api/meta/conversions (health check)\`); console.log(\` POST /api/meta/conversions\`); console.log(\`🔧 Modo: \${process.env.NODE_ENV || 'development'}\`); }); `; } // Template para .env.example function getEnvExampleTemplate() { return `# Meta Pixel Configuration # Copie este arquivo para .env e preencha com suas credenciais # Meta Pixel ID (obrigatório) VITE_META_PIXEL_ID=your_pixel_id_here # Meta API Access Token (obrigatório) VITE_META_API_ACCESS_TOKEN=your_access_token_here # Test Event Code (opcional - para modo de teste) VITE_META_TEST_EVENT_CODE=your_test_event_code_here # Configurações do servidor PORT=3000 NODE_ENV=development # Como obter as credenciais: # 1. Meta Pixel ID: https://business.facebook.com/settings/pixels # 2. Access Token: https://developers.facebook.com/tools/explorer/ # 3. Test Event Code: https://business.facebook.com/events_manager2/list/pixel/test-events `; } // Template para README de integração function getIntegrationReadmeTemplate() { return `# Meta Pixel Integration Guide ## 🚀 Setup Automático Este projeto foi configurado com o setup automático do @jussimirvfx/meta-pixel-tracking. ### 📁 Arquivos Criados - \`api/get-ip.js\` - API route para captura de IP real - \`api/meta-conversions.js\` - API route para Meta Conversions - \`dev-server-full.js\` - Servidor Express completo - \`.env.example\` - Template de variáveis de ambiente ### 🔧 Scripts Disponíveis \`\`\`bash # Setup inicial (já executado) npm run setup:meta # Servidor de desenvolvimento com API routes npm run dev:meta # Servidor de produção npm run dev:meta:prod \`\`\` ## 📋 Configuração 1. **Copie o arquivo de ambiente:** \`\`\`bash cp .env.example .env \`\`\` 2. **Configure suas credenciais no .env:** \`\`\`env VITE_META_PIXEL_ID=your_pixel_id_here VITE_META_API_ACCESS_TOKEN=your_access_token_here VITE_META_TEST_EVENT_CODE=your_test_event_code_here \`\`\` 3. **Inicie o servidor de desenvolvimento:** \`\`\`bash npm run dev:meta \`\`\` ## 🎯 Como Usar ### 1. Configurar o Meta Pixel \`\`\`javascript import { configureMetaPixel } from '@jussimirvfx/meta-pixel-tracking' configureMetaPixel({ PIXEL_ID: import.meta.env.VITE_META_PIXEL_ID, ACCESS_TOKEN: import.meta.env.VITE_META_API_ACCESS_TOKEN, TEST_EVENT_CODE: import.meta.env.VITE_META_TEST_EVENT_CODE }) \`\`\` ### 2. Usar o Hook \`\`\`javascript import { useMetaPixel } from '@jussimirvfx/meta-pixel-tracking' function App() { const { trackPageView, trackLead } = useMetaPixel() useEffect(() => { // PageView automático trackPageView() }, []) const handleLead = async () => { await trackLead({ email: 'user@example.com', phone: '+5511999999999', name: 'João Silva', value: 100, currency: 'BRL' }) } return ( <div> <button onClick={handleLead}>Enviar Lead</button> </div> ) } \`\`\` ## 🔍 Debug O sistema ativa debug automaticamente em: - URLs do Vercel (vercel.app, vercel.com) - Ambiente de desenvolvimento - Preview mode ### Logs Disponíveis \`\`\`javascript // Acessar logs no console window._metaPixelLogs.getLogs() // Verificar configuração window._metaPixelDebug.getConfig() // Ativar/desativar debug window._metaPixelLogs.enable() window._metaPixelLogs.disable() \`\`\` ## 📊 API Routes ### GET /api/get-ip Captura IP real do cliente. **Response:** \`\`\`json { "success": true, "ip": "201.45.123.45", "timestamp": 1640995200000 } \`\`\` ### POST /api/meta/conversions Envia eventos para Meta Conversions API. **Request:** \`\`\`json { "event_name": "PageView", "event_time": 1640995200, "event_id": "pageview_123", "event_source_url": "https://example.com", "user_data": { "em": ["hash_do_email"], "ph": ["hash_do_telefone"] } } \`\`\` ## 🚀 Deploy ### Vercel As API routes são automaticamente detectadas pelo Vercel. ### Outros Use o servidor Express incluído: \`\`\`bash npm run dev:meta:prod \`\`\` ## 📈 Benefícios - ✅ **IP Real**: Captura IP real do cliente - ✅ **Match Rate**: 85-95% de precisão - ✅ **Debug Automático**: Logs em Vercel/development - ✅ **Zero Config**: Setup automático - ✅ **Fallback**: Funciona mesmo sem API routes ## 🆘 Suporte Para problemas ou dúvidas: - Verifique os logs no console - Confirme as variáveis de ambiente - Teste as API routes individualmente `; } // Atualizar package.json com scripts function updatePackageJson() { const packagePath = path.join(process.cwd(), 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); // Adicionar scripts se não existirem if (!packageJson.scripts) { packageJson.scripts = {}; } const newScripts = { 'setup:meta': 'node node_modules/@jussimirvfx/meta-pixel-tracking/scripts/setup.js', 'dev:meta': 'node node_modules/@jussimirvfx/meta-pixel-tracking/scripts/dev-server.js', 'dev:meta:prod': 'NODE_ENV=production node dev-server-full.js' }; // Adicionar apenas scripts que não existem Object.keys(newScripts).forEach(script => { if (!packageJson.scripts[script]) { packageJson.scripts[script] = newScripts[script]; } }); // Adicionar dependências se necessário if (!packageJson.dependencies) { packageJson.dependencies = {}; } const requiredDeps = { 'express': '^4.18.2', 'http-proxy-middleware': '^2.0.6' }; Object.keys(requiredDeps).forEach(dep => { if (!packageJson.dependencies[dep] && !packageJson.devDependencies?.[dep]) { packageJson.dependencies[dep] = requiredDeps[dep]; } }); fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); logSuccess('package.json atualizado com scripts e dependências'); } // Função principal function main() { log('🚀 Setup automático do Meta Pixel/CAPI', 'bold'); log('====================================='); try { // 1. Verificar estrutura do projeto checkProjectStructure(); // 2. Criar diretório api const apiDir = createApiDirectory(); // 3. Criar API routes const ipApiPath = path.join(apiDir, 'get-ip.js'); const metaApiPath = path.join(apiDir, 'meta-conversions.js'); if (!fs.existsSync(ipApiPath)) { fs.writeFileSync(ipApiPath, getIpApiTemplate()); logSuccess('API route get-ip.js criada'); } else { logInfo('API route get-ip.js já existe'); } if (!fs.existsSync(metaApiPath)) { fs.writeFileSync(metaApiPath, getMetaConversionsTemplate()); logSuccess('API route meta-conversions.js criada'); } else { logInfo('API route meta-conversions.js já existe'); } // 4. Criar servidor Express const devServerPath = path.join(process.cwd(), 'dev-server-full.js'); if (!fs.existsSync(devServerPath)) { fs.writeFileSync(devServerPath, getDevServerTemplate()); logSuccess('Servidor Express dev-server-full.js criado'); } else { logInfo('Servidor Express dev-server-full.js já existe'); } // 5. Criar .env.example const envExamplePath = path.join(process.cwd(), '.env.example'); if (!fs.existsSync(envExamplePath)) { fs.writeFileSync(envExamplePath, getEnvExampleTemplate()); logSuccess('.env.example criado'); } else { logInfo('.env.example já existe'); } // 6. Criar README de integração const readmePath = path.join(process.cwd(), 'README-integration.md'); if (!fs.existsSync(readmePath)) { fs.writeFileSync(readmePath, getIntegrationReadmeTemplate()); logSuccess('README-integration.md criado'); } else { logInfo('README-integration.md já existe'); } // 7. Atualizar package.json updatePackageJson(); // 8. Resumo final log('', 'bold'); log('🎉 Setup concluído com sucesso!', 'green'); log('', 'bold'); log('📋 Próximos passos:', 'bold'); log('1. Copie .env.example para .env'); log('2. Configure suas credenciais do Meta Pixel'); log('3. Execute: npm run dev:meta'); log('4. Acesse: http://localhost:3000'); log('', 'bold'); log('📚 Documentação: README-integration.md', 'blue'); log('🔧 Scripts disponíveis:', 'blue'); log(' npm run setup:meta - Setup completo'); log(' npm run dev:meta - Servidor de desenvolvimento'); log(' npm run dev:meta:prod - Servidor de produção'); } catch (error) { logError(`Erro durante o setup: ${error.message}`); process.exit(1); } } // Executar se chamado diretamente if (require.main === module) { main(); } module.exports = { main };