@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
JavaScript
#!/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();
}