UNPKG

@jussimirvfx/meta-pixel-tracking

Version:

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

654 lines (544 loc) 19.5 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const readline = require('readline'); // Cores para output no terminal const colors = { green: '\x1b[32m', blue: '\x1b[34m', yellow: '\x1b[33m', red: '\x1b[31m', reset: '\x1b[0m' }; const log = { success: (msg) => console.log(`${colors.green}✅ ${msg}${colors.reset}`), info: (msg) => console.log(`${colors.blue}🔧 ${msg}${colors.reset}`), warning: (msg) => console.log(`${colors.yellow}⚠️ ${msg}${colors.reset}`), error: (msg) => console.log(`${colors.red}❌ ${msg}${colors.reset}`) }; // Interface para perguntas const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const question = (query) => new Promise(resolve => rl.question(query, resolve)); // Detectar tipo de projeto function detectProjectType() { const packageJsonPath = path.join(process.cwd(), 'package.json'); if (!fs.existsSync(packageJsonPath)) { log.error('package.json não encontrado. Execute este comando na raiz do projeto.'); process.exit(1); } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; let projectType = 'unknown'; let buildTool = 'unknown'; // Detectar framework if (dependencies.next) projectType = 'nextjs'; else if (dependencies.react) projectType = 'react'; // Detectar build tool if (dependencies.vite) buildTool = 'vite'; else if (dependencies['react-scripts']) buildTool = 'cra'; else if (dependencies.next) buildTool = 'nextjs'; return { projectType, buildTool, dependencies }; } // Encontrar arquivo principal function findMainFile() { const possibleFiles = [ // Arquivos na raiz (comum em projetos Vite) 'main.tsx', 'main.jsx', 'index.tsx', 'index.jsx', 'App.tsx', 'App.jsx', // Arquivos em src/ (padrão React) 'src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx', 'src/App.tsx', 'src/App.jsx', // Arquivos Next.js 'pages/_app.tsx', 'pages/_app.jsx', 'app/layout.tsx', 'app/layout.jsx' ]; for (const file of possibleFiles) { if (fs.existsSync(path.join(process.cwd(), file))) { return file; } } return null; } // Verificar se Provider já está configurado function isProviderConfigured(filePath) { const content = fs.readFileSync(path.join(process.cwd(), filePath), 'utf8'); return content.includes('MetaPixelProvider') || content.includes('meta-pixel-tracking'); } // Adicionar Provider ao arquivo principal function addProviderToFile(filePath, buildTool) { const fullPath = path.join(process.cwd(), filePath); let content = fs.readFileSync(fullPath, 'utf8'); // Backup do arquivo original fs.writeFileSync(`${fullPath}.backup`, content); const importStatement = `import { MetaPixelProvider } from '@jussimirvfx/meta-pixel-tracking';\n`; // Adicionar import se não existir if (!content.includes('MetaPixelProvider')) { const importRegex = /^import.*from.*['"];?\n/gm; const imports = content.match(importRegex) || []; const lastImportIndex = imports.length > 0 ? content.lastIndexOf(imports[imports.length - 1]) + imports[imports.length - 1].length : 0; content = content.slice(0, lastImportIndex) + importStatement + content.slice(lastImportIndex); } // Envolver JSX com Provider baseado no tipo de projeto if (buildTool === 'vite') { // Para Vite (main.tsx) - padrão multilinhas com vírgula final const renderMatch = content.match(/(ReactDOM\.createRoot\([^)]+\)\.render\(\s*)([\s\S]*?)(\s*,?\s*\))/); if (renderMatch) { const [fullMatch, before, jsxContent, after] = renderMatch; const wrappedJsx = `${before}\n <MetaPixelProvider>${jsxContent}\n </MetaPixelProvider>${after}`; content = content.replace(fullMatch, wrappedJsx); } } else if (buildTool === 'nextjs') { // Para Next.js (_app.tsx) content = content.replace( /return\s*(<Component[\s\S]*?\/>)/, `return (\n <MetaPixelProvider>\n $1\n </MetaPixelProvider>\n )` ); } else { // Para CRA e outros content = content.replace( /return\s*\(\s*(<div[\s\S]*?<\/div>|<[^>]+>[\s\S]*?<\/[^>]+>)\s*\)/, `return (\n <MetaPixelProvider>\n $1\n </MetaPixelProvider>\n )` ); } fs.writeFileSync(fullPath, content); log.success(`Provider adicionado a ${filePath}`); } // Criar arquivo .env function createEnvFile() { const envPath = path.join(process.cwd(), '.env'); const envContent = `# Meta Pixel Configuration # Obtenha esses valores no Facebook Business Manager # ID do Pixel do Meta (obrigatório) VITE_META_PIXEL_ID=seu_pixel_id_aqui # Token de acesso para a API de Conversões (obrigatório para server-side tracking) VITE_META_API_ACCESS_TOKEN=seu_access_token_aqui # Código de teste para eventos (opcional - usado durante desenvolvimento) VITE_META_TEST_EVENT_CODE=seu_test_code_aqui `; if (fs.existsSync(envPath)) { // Se .env já existe, adicionar apenas as variáveis que não existem let existingContent = fs.readFileSync(envPath, 'utf8'); const varsToAdd = [ 'VITE_META_PIXEL_ID', 'VITE_META_API_ACCESS_TOKEN', 'VITE_META_TEST_EVENT_CODE' ]; let needsUpdate = false; varsToAdd.forEach(varName => { if (!existingContent.includes(varName)) { const varLine = envContent.split('\n').find(line => line.includes(varName)); if (varLine) { existingContent += `\n${varLine}`; needsUpdate = true; } } }); if (needsUpdate) { fs.writeFileSync(envPath, existingContent); log.success('Variáveis Meta Pixel adicionadas ao .env existente'); } else { log.info('.env já contém as variáveis do Meta Pixel'); } } else { fs.writeFileSync(envPath, envContent); log.success('Arquivo .env criado com variáveis do Meta Pixel'); } } // Criar API routes function createApiRoutes() { const apiDir = path.join(process.cwd(), 'api'); if (!fs.existsSync(apiDir)) { fs.mkdirSync(apiDir, { recursive: true }); } // Criar api/get-ip.cjs const getIpContent = `const express = require('express'); const router = express.Router(); // Captura IP real do cliente router.get('/get-ip', (req, res) => { const clientIP = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection.remoteAddress || req.socket.remoteAddress || req.ip; res.json({ ip: 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'] } }); }); module.exports = router; `; fs.writeFileSync(path.join(apiDir, 'get-ip.cjs'), getIpContent); log.success('API route /api/get-ip.cjs criada'); // Criar api/meta-conversions.cjs const metaConversionsContent = `const express = require('express'); const router = express.Router(); const crypto = require('crypto'); // Função para capturar IP real function getClientIP(req) { return req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection.remoteAddress || req.socket.remoteAddress || req.ip; } // Função para hash SHA-256 function hashData(data) { if (!data) return null; return crypto.createHash('sha256').update(data.toLowerCase().trim()).digest('hex'); } // Função para preparar dados do usuário function prepareUserData(userData, clientIP) { const prepared = {}; // Dados que devem ser hasheados const hashFields = ['email', 'phone', 'name', 'city', 'state', 'zip', 'country', 'external_id']; hashFields.forEach(field => { if (userData[field]) { prepared[field] = hashData(userData[field]); } }); // Dados que não devem ser hasheados const nonHashFields = ['lead_score', 'qualification_reason', 'value', 'currency']; nonHashFields.forEach(field => { if (userData[field] !== undefined) { prepared[field] = userData[field]; } }); // Sempre incluir IP e User Agent if (clientIP) { prepared.client_ip_address = clientIP; } if (userData.user_agent) { prepared.client_user_agent = userData.user_agent; } // Cookies do Facebook if (userData.fbp) { prepared.fbp = userData.fbp; } if (userData.fbc) { prepared.fbc = userData.fbc; } return prepared; } // Rota para enviar eventos para Meta Conversions API router.post('/meta-conversions', async (req, res) => { try { const { event_name, user_data, event_data, event_id } = req.body; if (!event_name) { return res.status(400).json({ error: 'event_name é obrigatório' }); } const clientIP = getClientIP(req); const preparedUserData = prepareUserData(user_data, clientIP); const payload = { data: [{ event_name, event_time: Math.floor(Date.now() / 1000), event_id: event_id || crypto.randomUUID(), user_data: preparedUserData, ...(event_data && { custom_data: event_data }) }], test_event_code: process.env.VITE_META_TEST_EVENT_CODE }; const response = await fetch(\`https://graph.facebook.com/v23.0/\${process.env.VITE_META_PIXEL_ID}/events\`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': \`Bearer \${process.env.VITE_META_API_ACCESS_TOKEN}\` }, body: JSON.stringify(payload) }); const result = await response.json(); if (!response.ok) { console.error('Erro na Meta Conversions API:', result); return res.status(response.status).json(result); } res.json({ success: true, result, sent_data: { event_name, user_data_keys: Object.keys(preparedUserData), event_id: payload.data[0].event_id } }); } catch (error) { console.error('Erro interno:', error); res.status(500).json({ error: 'Erro interno do servidor', details: error.message }); } }); module.exports = router; `; fs.writeFileSync(path.join(apiDir, 'meta-conversions.cjs'), metaConversionsContent); log.success('API route /api/meta-conversions.cjs criada'); } // Criar servidor Express completo function createDevServer() { const serverContent = `const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const path = require('path'); // Importar rotas da API const getIpRouter = require('./api/get-ip.cjs'); const metaConversionsRouter = require('./api/meta-conversions.cjs'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware para parsing JSON app.use(express.json()); // Middleware para CORS app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // Rotas da API app.use('/api', getIpRouter); app.use('/api', metaConversionsRouter); // Servir arquivos estáticos do build (se existir) const distPath = path.join(__dirname, 'dist'); if (require('fs').existsSync(distPath)) { app.use(express.static(distPath)); // SPA fallback app.get('*', (req, res) => { res.sendFile(path.join(distPath, 'index.html')); }); } else { // Proxy para Vite dev server app.use('/', createProxyMiddleware({ target: 'http://localhost:5173', changeOrigin: true, logLevel: 'silent' })); } // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), environment: process.env.NODE_ENV || 'development' }); }); app.listen(PORT, () => { console.log(\`🚀 Servidor Express rodando na porta \${PORT}\`); console.log(\`📡 API routes disponíveis:\`); console.log(\` GET /api/get-ip\`); console.log(\` POST /api/meta-conversions\`); console.log(\`🔗 Acesse: http://localhost:\${PORT}\`); if (!require('fs').existsSync(distPath)) { console.log(\`⚠️ Vite dev server deve estar rodando na porta 5173\`); } }); `; fs.writeFileSync(path.join(process.cwd(), 'dev-server-full.cjs'), serverContent); log.success('Servidor Express completo criado (dev-server-full.cjs)'); } // Atualizar package.json com scripts function updatePackageJson() { const packageJsonPath = path.join(process.cwd(), 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); // Adicionar scripts se não existirem if (!packageJson.scripts) { packageJson.scripts = {}; } const newScripts = { 'setup:meta': 'node scripts/setup.js', 'dev:meta': 'node scripts/dev-server.js', 'dev:meta:prod': 'NODE_ENV=production node dev-server-full.cjs' }; let needsUpdate = false; Object.entries(newScripts).forEach(([key, value]) => { if (!packageJson.scripts[key]) { packageJson.scripts[key] = value; needsUpdate = true; } }); // Adicionar dependências se não existirem if (!packageJson.dependencies) { packageJson.dependencies = {}; } const newDependencies = { 'express': '^4.18.2', 'http-proxy-middleware': '^2.0.6' }; Object.entries(newDependencies).forEach(([key, value]) => { if (!packageJson.dependencies[key]) { packageJson.dependencies[key] = value; needsUpdate = true; } }); if (needsUpdate) { fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); log.success('package.json atualizado com scripts e dependências'); } else { log.info('package.json já contém os scripts necessários'); } } // Criar README de integração function createIntegrationReadme() { const readmeContent = `# Integração Meta Pixel/CAPI Este projeto foi configurado com o sistema completo de Meta Pixel/CAPI. ## 🚀 Scripts Disponíveis \`\`\`bash npm run setup:meta # Setup completo (já executado) npm run dev:meta # Servidor de desenvolvimento npm run dev:meta:prod # Servidor de produção \`\`\` ## 📁 Arquivos Criados - \`api/get-ip.cjs\` - Captura IP real do cliente - \`api/meta-conversions.cjs\` - API route do Meta Conversions - \`dev-server-full.cjs\` - Servidor Express completo - \`.env\` - Variáveis de ambiente (configure suas credenciais) ## 🔧 Como Usar 1. **Configure suas credenciais no .env:** \`\`\`bash VITE_META_PIXEL_ID=seu_pixel_id VITE_META_API_ACCESS_TOKEN=seu_access_token VITE_META_TEST_EVENT_CODE=seu_test_code \`\`\` 2. **Inicie o servidor:** \`\`\`bash npm run dev:meta \`\`\` 3. **Use o hook em seus componentes:** \`\`\`tsx import { useMetaPixel } from '@jussimirvfx/meta-pixel-tracking'; function MyComponent() { const { trackLead, trackLeadQualificado } = useMetaPixel(); const handleLead = async () => { await trackLead({ name: 'João Silva', email: 'joao@email.com', phone: '11999999999', value: 25, currency: 'BRL' }); }; return <button onClick={handleLead}>Enviar Lead</button>; } \`\`\` ## 📊 Benefícios - ✅ **IP Real Capturado** - Match rate 90%+ - ✅ **API Routes Prontas** - Zero configuração - ✅ **Servidor Express** - Desenvolvimento local - ✅ **Debug Automático** - Logs detalhados - ✅ **Vercel Ready** - Deploy automático ## 🔍 Debug O sistema ativa debug automaticamente em: - URLs do Vercel (vercel.app, vercel.com) - Ambiente de desenvolvimento - Preview mode \`\`\`javascript // Acessar logs no console window._metaPixelLogs.getLogs() // Verificar configuração window._metaPixelDebug.getConfig() \`\`\` `; fs.writeFileSync(path.join(process.cwd(), 'README-integration.md'), readmeContent); log.success('README de integração criado'); } // Função principal async function main() { console.log(`${colors.blue} 🚀 Meta Pixel Setup Completo Configuração automática do @jussimirvfx/meta-pixel-tracking ${colors.reset}`); try { // Detectar projeto const { projectType, buildTool } = detectProjectType(); log.success(`Detectamos projeto ${projectType.toUpperCase()} com ${buildTool.toUpperCase()}`); // Encontrar arquivo principal const mainFile = findMainFile(); if (!mainFile) { log.error('Não foi possível encontrar o arquivo principal do React.'); log.info('Arquivos suportados:'); log.info(' Raiz: main.tsx, main.jsx, index.tsx, index.jsx, App.tsx, App.jsx'); log.info(' src/: src/main.tsx, src/main.jsx, src/index.tsx, src/index.jsx, src/App.tsx, src/App.jsx'); log.info(' Next.js: pages/_app.tsx, pages/_app.jsx, app/layout.tsx, app/layout.jsx'); process.exit(1); } log.success(`Arquivo principal: ${mainFile}`); // Verificar se já está configurado if (isProviderConfigured(mainFile)) { log.warning('MetaPixelProvider já parece estar configurado!'); const overwrite = await question('Deseja reconfigurar mesmo assim? (y/N): '); if (overwrite.toLowerCase() !== 'y') { log.info('Configuração cancelada.'); rl.close(); return; } } console.log(''); log.info('Vamos configurar o Meta Pixel com setup completo:'); console.log(''); // Pergunta 1: Setup completo const setupComplete = await question('1. Executar setup completo (API routes + servidor)? (Y/n): '); if (setupComplete.toLowerCase() !== 'n') { createApiRoutes(); createDevServer(); updatePackageJson(); createIntegrationReadme(); } // Pergunta 2: Adicionar Provider const addProvider = await question('2. Adicionar Provider ao arquivo principal? (Y/n): '); if (addProvider.toLowerCase() !== 'n') { addProviderToFile(mainFile, buildTool); } // Pergunta 3: Criar/atualizar .env const createEnv = await question('3. Criar/atualizar arquivo .env com variáveis? (Y/n): '); if (createEnv.toLowerCase() !== 'n') { createEnvFile(); } console.log(''); log.success('🎉 Setup completo concluído!'); console.log(''); log.info('📝 Próximos passos:'); console.log(' 1. Configure suas variáveis no arquivo .env'); console.log(' 2. Execute: npm run dev:meta'); console.log(' 3. Acesse: http://localhost:3000'); console.log(' 4. Use o hook useMetaPixel() em seus formulários'); console.log(''); log.info('🔧 Arquivos criados:'); console.log(' - api/get-ip.cjs'); console.log(' - api/meta-conversions.cjs'); console.log(' - dev-server-full.cjs'); console.log(' - README-integration.md'); console.log(' - .env (configure suas credenciais)'); console.log(''); log.info('🔧 Backup criado: Os arquivos originais foram salvos com extensão .backup'); } catch (error) { log.error(`Erro durante a configuração: ${error.message}`); process.exit(1); } finally { rl.close(); } } // Executar se for chamado diretamente if (require.main === module) { main(); }