Automação de Relatórios: Extraindo Métricas de Campanhas da BMS para o Google Sheets
Este guia prático ensina como configurar a integração entre a plataforma BMS e o Google Sheets utilizando o Google Apps Script. Aprenda a automatizar a coleta de dados e gere relatórios de métricas de forma eficiente e personalizada.
A análise de dados é a chave para o sucesso de qualquer estratégia digital. Para facilitar esse processo, criamos uma solução via Google Apps Script que conecta sua conta BMS ao Google Sheets. Nas próximas etapas, você aprenderá a configurar a ferramenta para importar, de forma exclusiva, as métricas das suas campanhas. Com isso, você ganha autonomia para criar dashboards e relatórios personalizados de maneira simples e rápida.
Passo 1: Preparando o Google Sheets
- Abra uma planilha em branco no Google Sheets (ou a planilha onde deseja gerar os dados).
- No menu superior, clique em Extensões > Apps Script.
- Uma nova aba do navegador será aberta. Este é o editor de código onde instalaremos o sistema.
Passo 2: Inserindo os Códigos
No editor de código, você precisará configurar exatamente dois arquivos na barra lateral esquerda. Siga as instruções e copie os códigos abaixo.
Arquivo 1: Code.gs (O Motor)
- Clique no arquivo Code.gs que já vem criado por padrão na barra lateral esquerda.
- Apague tudo o que estiver dentro dele.
- Copie o código abaixo e cole lá:
JavaScript
/** * INTEGRAÇÃO BMS METRICS - GOOGLE SHEETS * Versão Final (Layout em Colunas + Atualização Automática) */const CONFIG = { apiMonUrl: "https://api.mon.bluems.com/v1", apiDspUrl: "https://api.dsp.bluems.com/v1", sheetData: "Dados", limitRows: 200000, chunkDays: 31, idsPerRequest: 50, periodSeconds: 86400, pageSizeMetrics: 200, cacheTime: 1800};function onOpen() { SpreadsheetApp.getUi().createMenu('BMS Metrics 🚀') .addItem('📋 Abrir Painel', 'showSidebar') .addToUi();}function showSidebar() { const html = HtmlService.createHtmlOutputFromFile('Sidebar').setTitle('BMS Metrics').setWidth(350); SpreadsheetApp.getUi().showSidebar(html);}function checkConfig() { const p = PropertiesService.getScriptProperties(); return (p.getProperty('BMS_API_KEY') && p.getProperty('BMS_ACCOUNT_ID')) ? true : false;}function saveSettings(key, id) { PropertiesService.getScriptProperties().setProperties({'BMS_API_KEY': key, 'BMS_ACCOUNT_ID': id}); return true;}function getSidebarData() { const p = PropertiesService.getScriptProperties(); const key = p.getProperty('BMS_API_KEY'), accId = p.getProperty('BMS_ACCOUNT_ID'); if (!key || !accId) throw new Error("Credenciais ausentes."); const cache = CacheService.getUserCache().get("BMS_DATA_FINAL"); if (cache) return JSON.parse(cache); const camps = []; try { const resp = UrlFetchApp.fetch(`${CONFIG.apiDspUrl}/accounts/${accId}/campaigns`, {headers:{"X-Api-Key":key}, muteHttpExceptions:true}); if(resp.getResponseCode()===200) { JSON.parse(resp.getContentText()).values.forEach(c => camps.push({id: c.campaignId||c.id, name: c.name||c.id, tz: c.timezone||"UTC", created: c.createdAt})); } } catch(e) {} let metrics = []; try { const resp = UrlFetchApp.fetch(`${CONFIG.apiMonUrl}/metrics?pageSize=${CONFIG.pageSizeMetrics}`, {headers:{"X-Api-Key":key}, muteHttpExceptions:true}); metrics = JSON.parse(resp.getContentText()).values || []; } catch(e) {} const uniqueMets = Array.from(new Map(metrics.map(m => [m.metricId, m])).values()).sort((a,b) => a.name.localeCompare(b.name)); const data = { campaigns: camps.sort((a,b) => a.name.localeCompare(b.name)), metrics: uniqueMets }; try { CacheService.getUserCache().put("BMS_DATA_FINAL", JSON.stringify(data), CONFIG.cacheTime); } catch(e){} return data;}function processMultiReport(selCamps, selMets, startDateStr) { // Salva a seleção para a execução automática de madrugada PropertiesService.getScriptProperties().setProperty('BMS_LAST_CAMPS', JSON.stringify(selCamps)); PropertiesService.getScriptProperties().setProperty('BMS_LAST_METS', JSON.stringify(selMets)); const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName(CONFIG.sheetData); if (!sheet) { sheet = ss.insertSheet(CONFIG.sheetData); sheet.appendRow(["Data", "Campanha"]); sheet.setFrozenRows(1); sheet.getRange("A:A").setNumberFormat("dd/MM/yyyy"); } const lastCol = sheet.getLastColumn() || 2; let headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0]; selMets.forEach(m => { let colName = `${m.name} (${m.stat})`; if (!headers.includes(colName)) headers.push(colName); }); let dataMap = new Map(); const lastRow = sheet.getLastRow(); if (lastRow > 1) { const existingData = sheet.getRange(2, 1, lastRow - 1, lastCol).getValues(); existingData.forEach(row => { if (!row[0]) return; let dStr = row[0] instanceof Date ? Utilities.formatDate(row[0], "UTC", "yyyy-MM-dd") : String(row[0]); let key = dStr + "||" + row[1]; while (row.length < headers.length) row.push(""); dataMap.set(key, row); }); } const p = PropertiesService.getScriptProperties(); const key = p.getProperty('BMS_API_KEY'), accId = p.getProperty('BMS_ACCOUNT_ID'); let reqDate = new Date(startDateStr); reqDate.setHours(0,0,0,0); reqDate.setDate(reqDate.getDate() + 1); const now = new Date(); now.setHours(0,0,0,0); const campsByTz = {}; selCamps.forEach(c => { if(!campsByTz[c.tz]) campsByTz[c.tz] = []; campsByTz[c.tz].push(c); }); for (const [tz, campaigns] of Object.entries(campsByTz)) { for (let i = 0; i < campaigns.length; i += CONFIG.idsPerRequest) { const chunk = campaigns.slice(i, i + CONFIG.idsPerRequest); const ids = chunk.map(c => c.id), map = new Map(chunk.map(c => [c.id, c.name])); let current = new Date(reqDate); current.setDate(current.getDate() - 1); while (current < now) { let end = new Date(current); end.setDate(end.getDate() + CONFIG.chunkDays); if (end > now) end = now; if (end <= current) break; const results = fetchInParallel(key, accId, selMets, ids, calculateIsoDates(current, end, tz)); results.forEach((res, idx) => { if (!res || !res[0] || !res[0].results) return; const met = selMets[idx]; let colName = `${met.name} (${met.stat})`; let colIdx = headers.indexOf(colName); res[0].results.forEach(r => { let id = r.group && r.group[0] ? (r.group[0].values ? r.group[0].values[0] : r.group[0].value) : null; let cName = map.get(id) || id || "-"; (r.timestamps || []).forEach((t, k) => { let dVal, dStr; try { dVal = new Date(Utilities.formatDate(new Date(t), tz, "yyyy-MM-dd") + "T00:00:00"); dStr = Utilities.formatDate(dVal, "UTC", "yyyy-MM-dd"); } catch(e){ dVal = new Date(t); dStr = String(t); } let rowKey = dStr + "||" + cName; let rowObj = dataMap.get(rowKey); if (!rowObj) { rowObj = new Array(headers.length).fill(""); rowObj[0] = dVal; rowObj[1] = cName; dataMap.set(rowKey, rowObj); } while(rowObj.length < headers.length) rowObj.push(""); rowObj[colIdx] = r.values[k]; }); }); }); current = new Date(end); Utilities.sleep(50); } } } let finalData = Array.from(dataMap.values()); if (finalData.length > 0) { finalData.sort((a, b) => new Date(a[0]) - new Date(b[0])); finalData.forEach(r => { while(r.length < headers.length) r.push(""); }); sheet.clearContents(); sheet.getRange(1, 1, 1, headers.length).setValues([headers]); sheet.getRange(2, 1, finalData.length, headers.length).setValues(finalData); sheet.getRange(1, 1, 1, headers.length).setFontWeight("bold").setBackground("#f3f3f3"); } if (sheet.getLastRow() > CONFIG.limitRows) sheet.deleteRows(2, (sheet.getLastRow() - CONFIG.limitRows) + 1000); return finalData.length;}function fetchInParallel(key, accId, metrics, ids, dates) { const reqs = metrics.map(m => ({ url: `${CONFIG.apiMonUrl}/accounts/${accId}/metrics:query-statistics`, method: "POST", headers: { "X-Api-Key": key, "Content-Type": "application/json" }, payload: JSON.stringify({ "start": dates.start, "end": dates.end, "limit": 10000, "queries": [{ "id": "q1", "metric": { "metricId": m.id, "statistic": m.stat, "period": CONFIG.periodSeconds, "groupBy": ["campaignId"], "where": [{ "name": "campaignId", "values": ids }] } }] }), muteHttpExceptions: true })); return UrlFetchApp.fetchAll(reqs).map(r => r.getResponseCode() === 200 ? JSON.parse(r.getContentText()) : null);}function calculateIsoDates(start, end, tz) { const f = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; try { return { start: Utilities.formatDate(start, tz, f), end: Utilities.formatDate(end, tz, f) }; } catch (e) { return { start: Utilities.formatDate(start, "UTC", f), end: Utilities.formatDate(end, "UTC", f) }; }}// --- FUNÇÃO DE ATUALIZAÇÃO AUTOMÁTICA (ACIONADOR DIÁRIO) ---function autoUpdateDiario() { const p = PropertiesService.getScriptProperties(); const campsStr = p.getProperty('BMS_LAST_CAMPS'); const metsStr = p.getProperty('BMS_LAST_METS'); if (!campsStr || !metsStr) { console.warn("Rode o relatório manualmente pelo menos uma vez para configurar a automação."); return; } const selCamps = JSON.parse(campsStr); const selMets = JSON.parse(metsStr); // Define a data inicial como 5 dias atrás para garantir que atualizações retroativas sejam capturadas let d = new Date(); d.setDate(d.getDate() - 5); let startDateStr = Utilities.formatDate(d, Session.getScriptTimeZone(), "yyyy-MM-dd"); processMultiReport(selCamps, selMets, startDateStr);}Arquivo 2: Sidebar.html (A Interface Visual)
- Na barra lateral esquerda do editor, clique no botão "+" (Adicionar um arquivo) e escolha HTML.
- Nomeie exatamente como Sidebar (o Google adiciona o .html sozinho).
- Apague o conteúdo que vier escrito e cole este código:
HTML
<!DOCTYPE html><html><head> <base target="_top"> <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css"> <style> body { padding: 10px; padding-bottom: 70px; font-family: 'Segoe UI', sans-serif; background: #f8f9fa; } .hidden { display: none !important; } .top-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .app-title { font-weight: 700; color: #1a73e8; font-size: 15px; } .icon-btn { background: none; border: none; cursor: pointer; color: #9aa0a6; font-size: 16px; padding: 5px; } .icon-btn:hover { color: #202124; } .card { background: #fff; padding: 10px; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); margin-bottom: 10px; } h3 { margin: 0 0 8px 0; color: #202124; font-size: 13px; font-weight: 600; } .list-box { height: 160px; overflow-y: auto; border: 1px solid #dadce0; border-radius: 4px; background: #fff; } .row { display: flex; align-items: center; padding: 5px; border-bottom: 1px solid #f1f3f4; font-size: 12px; } .row label { flex: 1; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; } .row input { margin-right: 8px; } .met-id { font-size: 10px; color: #5f6368; font-family: monospace; display: block; } .stat-sel { font-size: 10px; width: 70px; margin-left: 5px; } .input-std { width: 94%; padding: 6px; border: 1px solid #dadce0; border-radius: 4px; font-size: 12px; margin-bottom: 8px; } .btn-main { background: #1a73e8; color: #fff; width: 100%; height: 40px; border: none; border-radius: 4px; font-weight: 600; font-size: 13px; cursor: pointer; margin-top: 10px; display: flex; justify-content: center; align-items: center; gap: 8px; } .btn-main:hover { background: #1557b0; } .btn-main:disabled { background: #dadce0; color: #888; cursor: default; } .tools { font-size: 10px; text-align: right; margin-top: 2px; } .tools a { color: #1a73e8; cursor: pointer; margin-left: 6px; text-decoration: none; } #status { margin-top: 10px; font-size: 12px; text-align: center; color: #5f6368; } </style></head><body> <div id="scr-conf" class="hidden"> <div class="top-bar"><span class="app-title">⚙️ Configuração</span><button id="btnCloseConf" class="icon-btn" onclick="nav('rep')">✕</button></div> <div class="card"> <input type="text" id="accId" class="input-std" placeholder="Account ID"> <input type="text" id="apiKey" class="input-std" placeholder="API Key"> </div> <button class="btn-main" style="background:#1e8e3e" onclick="save()">✅ Salvar</button> </div> <div id="scr-rep" class="hidden"> <div class="top-bar"><span class="app-title">🚀 Relatório</span><button class="icon-btn" onclick="nav('conf')">⚙️</button></div> <div class="card"><h3>📅 Data Inicial</h3><input type="date" id="startDt" class="input-std"></div> <div class="card"><h3>📢 Campanhas</h3><input type="text" class="input-std" placeholder="🔍 Filtrar..." onkeyup="filter('lstCamp', this.value)"><div id="lstCamp" class="list-box"></div><div class="tools"><a onclick="viewSel('lstCamp',this)">🌪️ Filtrar Sel.</a><a onclick="tog('lstCamp',1)">Todos</a><a onclick="tog('lstCamp',0)">Nenhum</a></div></div> <div class="card"><h3>📊 Métricas</h3><input type="text" class="input-std" placeholder="🔍 Filtrar..." onkeyup="filter('lstMet', this.value)"><div id="lstMet" class="list-box"></div><div class="tools"><a onclick="viewSel('lstMet',this)">🌪️ Filtrar Sel.</a><a onclick="tog('lstMet',1)">Todos</a><a onclick="tog('lstMet',0)">Nenhum</a></div></div> <button id="btnRun" class="btn-main" onclick="run()">🚀 Gerar Relatório</button><div id="status"></div> </div><script> window.onload = () => { document.getElementById('startDt').valueAsDate = new Date(); google.script.run.withSuccessHandler(init).checkConfig(); }; function init(h) { if(h) { nav('rep'); google.script.run.withSuccessHandler(render).getSidebarData(); } else { nav('conf'); } } function nav(s) { document.getElementById('scr-conf').classList.toggle('hidden', s!=='conf'); document.getElementById('scr-rep').classList.toggle('hidden', s!=='rep'); } function save() { const k=document.getElementById('apiKey').value, i=document.getElementById('accId').value; google.script.run.withSuccessHandler(()=>{nav('rep');google.script.run.withSuccessHandler(render).getSidebarData();}).saveSettings(k,i); } function render(d) { let hC="", hM=""; d.campaigns.forEach(c => hC += `<div class="row"><input type="checkbox" value="${c.id}" n="c" tz="${c.tz}" cr="${c.created}"><label>${c.name}</label></div>`); d.metrics.forEach(m => { let def = m.statistics.includes('count') ? 'count' : (m.statistics.includes('sum') ? 'sum' : 'avg'); let ops = m.statistics.map(s => `<option value="${s}" ${s==def?'selected':''}>${s}</option>`).join(''); hM += `<div class="row"><input type="checkbox" value="${m.metricId}" n="m"><label><span style="font-weight:600">${m.name}</span><span class="met-id">${m.metricId}</span></label><select class="stat-sel">${ops}</select></div>`; }); document.getElementById('lstCamp').innerHTML = hC; document.getElementById('lstMet').innerHTML = hM; } function run() { const dt=document.getElementById('startDt').value; const cs=Array.from(document.querySelectorAll('input[n="c"]:checked')).map(e=>({id:e.value, name:e.nextSibling.innerText, tz:e.getAttribute('tz'), created:e.getAttribute('cr')})); const ms=Array.from(document.querySelectorAll('input[n="m"]:checked')).map(e=>({id:e.value, name:e.nextSibling.querySelector('span').innerText, stat:e.parentNode.querySelector('select').value})); if(!cs.length || !ms.length) return alert("Selecione campanhas e métricas!"); const btn=document.getElementById('btnRun'), st=document.getElementById('status'); btn.disabled=true; btn.innerText="⏳ Processando..."; st.innerHTML = `Organizando métricas em colunas...`; google.script.run .withSuccessHandler(n=>{ btn.disabled=false; btn.innerText="🚀 Gerar Relatório"; st.innerHTML=`<span style="color:green; font-weight:bold;">✅ Planilha Atualizada!</span>`; }) .withFailureHandler(e=>{ btn.disabled=false; btn.innerText="🚀 Gerar Relatório"; st.innerHTML=`<span style='color:red'>Erro: ${e.message}</span>`; }) .processMultiReport(cs,ms,dt); } function filter(id,v) { Array.from(document.getElementById(id).children).forEach(r=>r.style.display=r.innerText.toLowerCase().includes(v.toLowerCase())?'flex':'none'); } function tog(id,s) { Array.from(document.getElementById(id).children).forEach(r=>{if(r.style.display!=='none') r.querySelector('input').checked=!!s;}); } function viewSel(id,el) { const active=el.getAttribute('on')==='1'; Array.from(document.getElementById(id).children).forEach(r => r.style.display=active?'flex':(r.querySelector('input').checked?'flex':'none')); el.innerText=active?"🌪️ Filtrar Sel.":"🔙 Ver Todos"; el.setAttribute('on',active?'0':'1'); }</script></body></html>Atenção: Clique no ícone de Disquete no topo da tela para salvar o projeto após colar os dois códigos.
Passo 3: Permissões do Google
- Volte para a aba da sua Planilha do Google Sheets e atualize a página (pressione F5).
- Aguarde alguns segundos. Você verá um novo menu chamado "BMS Metrics 🚀" aparecer ao lado do menu "Ajuda".
- Clique em BMS Metrics 🚀 > 📋 Abrir Painel.
- Como é a primeira vez, o Google pedirá uma Autorização de segurança.
- Clique em Continuar.
- Escolha sua conta Google.
- Clique em Avançado (se aparecer um aviso) e depois em Acessar projeto.
- Clique em Permitir.
Passo 4: Configurando suas Credenciais
- Com as permissões concedidas, clique novamente em BMS Metrics 🚀 > 📋 Abrir Painel.
- Uma janela lateral vai se abrir na direita pedindo suas credenciais.
- Preencha o seu Account ID e a sua API Key da BMS.
- Clique em ✅ Salvar. O painel principal será carregado em seguida.
Passo 5: Gerando o seu Primeiro Relatório
- No painel, escolha a Data Inicial (ex: início do mês).
- Na seção de Campanhas, marque as caixinhas das campanhas que deseja analisar.
- Na seção de Métricas, marque as caixinhas do que deseja extrair (ex: Clicks, Impressions, CTR) e verifique se a estatística ao lado (sum, avg) está correta para o que você precisa.
- Clique em 🚀 Gerar Relatório.
- Aguarde o processamento. O script criará (ou atualizará) a aba chamada "Dados", organizando cada métrica em uma coluna de forma inteligente e à prova de duplicatas.
