@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
JavaScript
/**
* 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 `
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
PORT=3000
NODE_ENV=development
`;
}
// Template para README de integração
function getIntegrationReadmeTemplate() {
return `
Este projeto foi configurado com o setup automático do @jussimirvfx/meta-pixel-tracking.
- \`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
\`\`\`bash
npm run setup:meta
npm run dev:meta
npm run dev:meta:prod
\`\`\`
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
\`\`\`
\`\`\`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
})
\`\`\`
\`\`\`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>
)
}
\`\`\`
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
}
\`\`\`
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"]
}
}
\`\`\`
As API routes são automaticamente detectadas pelo Vercel.
Use o servidor Express incluído:
\`\`\`bash
npm run dev:meta:prod
\`\`\`
- ✅ **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
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 };