Created
April 18, 2025 03:33
-
-
Save 0yogue/8c541334880af7fdda269e3848fb8379 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Estrutura de extração de dados da Graph API do Meta usando Node.js | |
| // Para Fluxos.co - Integração Facebook, Instagram e WhatsApp | |
| // --- Dependências necessárias --- | |
| // npm install facebook-nodejs-business-sdk axios dotenv winston mongoose express node-cron | |
| // --- Configuração e inicialização --- | |
| require('dotenv').config(); // Para variáveis de ambiente | |
| const bizSdk = require('facebook-nodejs-business-sdk'); | |
| const axios = require('axios'); | |
| const winston = require('winston'); // Para logging | |
| const cron = require('node-cron'); // Para agendamento | |
| // Configuração da SDK do Facebook | |
| const AdAccount = bizSdk.AdAccount; | |
| const Page = bizSdk.Page; | |
| const Lead = bizSdk.Lead; | |
| const api = bizSdk.FacebookAdsApi.init(process.env.META_ACCESS_TOKEN); | |
| const showDebugingInfo = true; | |
| if (showDebugingInfo) { | |
| api.setDebug(true); | |
| } | |
| // Logger | |
| const logger = winston.createLogger({ | |
| level: 'info', | |
| format: winston.format.combine( | |
| winston.format.timestamp(), | |
| winston.format.json() | |
| ), | |
| defaultMeta: { service: 'meta-data-extractor' }, | |
| transports: [ | |
| new winston.transports.File({ filename: 'error.log', level: 'error' }), | |
| new winston.transports.File({ filename: 'combined.log' }), | |
| new winston.transports.Console() | |
| ], | |
| }); | |
| // --- Utilitários Gerais --- | |
| // Retry com backoff exponencial | |
| async function retryWithBackoff(operation, maxRetries = 3, initialDelay = 1000) { | |
| let retries = 0; | |
| while (true) { | |
| try { | |
| return await operation(); | |
| } catch (error) { | |
| retries++; | |
| if (retries > maxRetries) { | |
| throw error; | |
| } | |
| // Rate limit ou erro 4xx/5xx | |
| if (error.status && error.status >= 400) { | |
| const delay = initialDelay * Math.pow(2, retries - 1); | |
| logger.warn(`Rate limit atingido, tentando novamente em ${delay}ms. Tentativa ${retries} de ${maxRetries}`); | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| } else { | |
| throw error; | |
| } | |
| } | |
| } | |
| } | |
| // Formatar datas para yyyy-mm-dd | |
| function formatDate(date) { | |
| return date.toISOString().split('T')[0]; | |
| } | |
| // Normalizar IDs | |
| function normalizeId(id, prefix = '') { | |
| if (!id) return null; | |
| if (id.startsWith(prefix)) return id; | |
| return `${prefix}${id}`; | |
| } | |
| // Parse seguro para valores numéricos | |
| function parseNumericValue(value, defaultValue = 0) { | |
| const parsed = Number(value); | |
| return isNaN(parsed) ? defaultValue : parsed; | |
| } | |
| // Validação de parâmetros do cliente | |
| function validateParams(params) { | |
| const { adAccountId, formIds, pageId, whatsappPhoneId, whatsappToken } = params; | |
| const errors = []; | |
| if (!adAccountId) { | |
| errors.push('adAccountId é obrigatório'); | |
| } else if (!adAccountId.startsWith('act_')) { | |
| errors.push('adAccountId deve começar com "act_"'); | |
| } | |
| if (formIds && !Array.isArray(formIds)) { | |
| errors.push('formIds deve ser um array'); | |
| } | |
| if (!pageId) { | |
| errors.push('pageId é obrigatório'); | |
| } | |
| if (whatsappPhoneId && !whatsappToken) { | |
| errors.push('whatsappToken é obrigatório quando whatsappPhoneId é fornecido'); | |
| } | |
| return { | |
| isValid: errors.length === 0, | |
| errors | |
| }; | |
| } | |
| // --- 1. Extrator de Facebook/Instagram Ads --- | |
| /** | |
| * Extrai o maior conjunto possível de métricas de anúncios do Meta, | |
| * priorizando as métricas essenciais para o negócio e exportando também todas as demais. | |
| * Campos obrigatórios: impressões, CPM, CPC, CTR, cliques, conversões, etc. | |
| * Documentação oficial: https://developers.facebook.com/docs/marketing-api/insights/parameters/v18.0 | |
| */ | |
| async function extractAdsData(adAccountId, dateRange = { since: '2023-01-01', until: '2023-01-31' }) { | |
| try { | |
| const fields = [ | |
| // Métricas prioritárias | |
| 'campaign_name', | |
| 'impressions', | |
| 'cpm', | |
| 'unique_outbound_clicks', | |
| 'outbound_clicks_ctr', | |
| 'clicks', | |
| 'cpc', | |
| 'website_ctr', | |
| 'actions', | |
| 'action_values', // Valor das ações (ex: valor das compras) | |
| 'cost_per_action_type', // Custo por tipo de ação | |
| 'conversions', | |
| 'spend', | |
| // Métricas adicionais sugeridas | |
| // ... (demais campos) | |
| ]; | |
| // ... (restante da função) | |
| } catch (error) { | |
| logger.error(`Erro ao extrair dados de anúncios: ${error.message}`); | |
| return []; | |
| } | |
| } | |
| // ... (demais funções e módulos) | |
| // --- 6. Exportação de Dados --- | |
| function exportData(clientName, data) { | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const dir = './data'; | |
| console.log('Exportando dados para:', dir); | |
| logger.info('Exportando dados para: ' + dir); | |
| // Criar diretório se não existir | |
| if (!fs.existsSync(dir)) { | |
| fs.mkdirSync(dir); | |
| } | |
| // Criar diretório para o cliente | |
| const clientDir = path.join(dir, clientName); | |
| if (!fs.existsSync(clientDir)) { | |
| fs.mkdirSync(clientDir); | |
| } | |
| // Criar subdiretório para a data atual | |
| const today = new Date(); | |
| const dateDir = path.join(clientDir, today.toISOString().split('T')[0]); | |
| if (!fs.existsSync(dateDir)) { | |
| fs.mkdirSync(dateDir); | |
| } | |
| // Exportar cada tipo de dado para um arquivo JSON separado | |
| for (const [dataType, dataContent] of Object.entries(data)) { | |
| const filePath = path.join(dateDir, `${dataType}.json`); | |
| console.log('Salvando arquivo em:', filePath); | |
| logger.info('Salvando arquivo em: ' + filePath); | |
| fs.writeFileSync(filePath, JSON.stringify(dataContent, null, 2)); | |
| logger.info(`Dados exportados para: ${filePath}`); | |
| } | |
| // Gerar e exportar relatório de métricas prioritárias se tiver dados de anúncios | |
| if (data.adsData && data.adsData.length > 0) { | |
| const priorityReport = generatePriorityMetricsReport(clientName, data.adsData); | |
| if (priorityReport) { | |
| // Exportar como JSON | |
| const reportJsonPath = path.join(dateDir, 'priority_metrics_report.json'); | |
| console.log('Salvando arquivo em:', reportJsonPath); | |
| logger.info('Salvando arquivo em: ' + reportJsonPath); | |
| fs.writeFileSync(reportJsonPath, JSON.stringify(priorityReport, null, 2)); | |
| // Exportar também como CSV para fácil importação em Excel | |
| const { Parser } = require('json2csv'); | |
| // Exportar resumo | |
| const summaryFields = Object.keys(priorityReport.summary); | |
| const summaryParser = new Parser({ fields: summaryFields }); | |
| const summaryCsv = summaryParser.parse([priorityReport.summary]); | |
| const summaryCsvPath = path.join(dateDir, 'priority_metrics_summary.csv'); | |
| console.log('Salvando arquivo em:', summaryCsvPath); | |
| logger.info('Salvando arquivo em: ' + summaryCsvPath); | |
| fs.writeFileSync(summaryCsvPath, summaryCsv); | |
| // Exportar dados por campanha | |
| if (priorityReport.campaigns.length > 0) { | |
| const campaignFields = Object.keys(priorityReport.campaigns[0]); | |
| const campaignsParser = new Parser({ fields: campaignFields }); | |
| const campaignsCsv = campaignsParser.parse(priorityReport.campaigns); | |
| const campaignsCsvPath = path.join(dateDir, 'priority_metrics_by_campaign.csv'); | |
| console.log('Salvando arquivo em:', campaignsCsvPath); | |
| logger.info('Salvando arquivo em: ' + campaignsCsvPath); | |
| fs.writeFileSync(campaignsCsvPath, campaignsCsv); | |
| } | |
| logger.info(`Relatório de métricas prioritárias exportado para: ${reportJsonPath}`); | |
| } | |
| } | |
| // Criar um arquivo index.html simples para visualização dos dados | |
| const indexHtml = ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Dados Extraídos - ${clientName}</title> | |
| <style> | |
| body { font-family: Arial, sans-serif; margin: 20px; } | |
| h1 { color: #333; } | |
| .file-list { margin: 20px 0; } | |
| .file-item { margin: 10px 0; } | |
| a { color: #0066cc; text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Dados Extraídos - ${clientName}</h1> | |
| <p>Extração realizada em: ${new Date().toLocaleString()}</p> | |
| <div class="file-list"> | |
| <h2>Arquivos Disponíveis:</h2> | |
| ${fs.readdirSync(dateDir).map(file => ` | |
| <div class="file-item"> | |
| <a href="file://${path.join(dateDir, file)}">${file}</a> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </body> | |
| </html> | |
| `; | |
| const indexPath = path.join(dateDir, 'index.html'); | |
| console.log('Salvando arquivo em:', indexPath); | |
| logger.info('Salvando arquivo em: ' + indexPath); | |
| fs.writeFileSync(indexPath, indexHtml); | |
| logger.info(`Página de índice criada em: ${indexPath}`); | |
| return dateDir; // Retorna o caminho do diretório onde os dados foram salvos | |
| } | |
| // --- TESTE MANUAL DE EXPORTAÇÃO --- | |
| if (process.argv.includes('--test-export')) { | |
| const exportData = module.exports ? module.exports.exportData : global.exportData; | |
| if (typeof exportData !== 'function') { | |
| console.error('Função exportData não encontrada!'); | |
| process.exit(1); | |
| } | |
| const testData = { | |
| adsData: [{ id: 1, name: 'Teste Ad', impressions: 1000 }], | |
| conversionData: [{ id: 1, type: 'purchase', value: 200 }], | |
| audienceData: [{ id: 1, segment: '18-24', size: 500 }], | |
| customAudiences: [{ id: 1, name: 'Custom Audience 1' }], | |
| urlParameters: [{ id: 1, param: 'utm_source', value: 'test' }] | |
| }; | |
| const dir = exportData('TEST_CLIENTE', testData); | |
| console.log('Exportação de teste concluída. Diretório:', dir); | |
| process.exit(0); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment