O dashboard do gestor comercial da Vetra, a distribuidora fictícia que mantenho como demonstração do portfólio, parou de bater num fechamento de mês. A comissão por vendedor mostrava um total, o relatório de origem mostrava outro. Alguém tinha editado o filtro do relatório direto na org semanas antes, e não havia registro de quem foi, de quando, nem de qual era o filtro anterior. O painel inteiro morava só dentro do Salesforce, sem uma linha de histórico, e voltar ao estado que funcionava virou arqueologia.
Para versionar relatório e dashboard do Salesforce no Git, você recupera cada um como metadado
(.report-meta.xml e .dashboard-meta.xml) com o sf CLI para as pastas
reports/ e dashboards/ do repositório, comita, e passa a fazer deploy pelo mesmo
pipeline do Apex. O painel deixa de ser um clique irreversível na org e vira código com histórico,
diff e rollback.
Por que a camada de analytics fica de fora do Git
Quase todo time que já usa controle de versão versiona Apex, LWC, objetos e Flows, e para por aí. Relatório e dashboard continuam sendo criados e editados na interface, direto na produção, por quem tem permissão de montar painel. A causa não é preguiça, é a ilusão de que analytics é uma camada de leitura descartável: se um relatório quebrar, é só refazer. O problema é que o painel operacional não é descartável. Ele alimenta a reunião de resultado, o cálculo de comissão, a decisão de onde alocar vendedor, e quando alguém mexe num filtro sem querer, o número muda para todo mundo ao mesmo tempo, sem aviso e sem rastro.
A edição na org é um clique irreversível. Você não tem como pedir uma revisão antes da mudança subir, não tem como comparar o filtro de hoje com o da semana passada, e não tem como voltar ao estado anterior sem recriar tudo de memória. É exatamente o buraco que o controle de versão fecha em código, e a boa notícia é que relatório e dashboard são metadado como qualquer outro. Só precisam ser tratados como tal.
O relatório vira metadado: a anatomia do .report-meta.xml
Um relatório, no repositório, é um arquivo XML que descreve o tipo, o formato, as colunas, os agrupamentos e os filtros. Veja o esqueleto do relatório de comissão por vendedor da Vetra, o mesmo que alimenta o painel que quebrou. Ele parte de Oportunidades com produtos, agrupa por vendedor, e filtra pelas ganhas do mês:
<?xml version="1.0" encoding="UTF-8"?>
<Report xmlns="http://soap.sforce.com/2006/04/metadata">
<name>Comissao por Vendedor</name>
<reportType>OpportunityProduct</reportType>
<format>Summary</format>
<groupingsDown>
<field>FULL_NAME</field> <!-- nome do Owner, NAO "OwnerId" -->
<sortOrder>Asc</sortOrder>
</groupingsDown>
<columns>
<field>OPPORTUNITY_NAME</field>
<field>AMOUNT</field>
<field>Opportunity.Discount_Percent__c</field> <!-- campo custom: nome qualificado -->
</columns>
<timeFrameFilter>
<dateColumn>CLOSE_DATE</dateColumn>
<interval>INTERVAL_LASTTHISMONTH</interval>
</timeFrameFilter>
<filter>
<criteriaItems>
<column>WON</column>
<operator>equals</operator>
<value>true</value>
</criteriaItems>
</filter>
</Report>
Repare no reportType: para um objeto padrão como Oportunidades ele é o próprio nome
(Opportunity, ou OpportunityProduct para Oportunidades com produtos); para um objeto
custom, ele vem no formato CustomEntity$NomeDoObjeto__c. O timeFrameFilter com o
intervalo INTERVAL_LASTTHISMONTH é o que dá a janela "mês anterior mais o corrente" sem chumbar
datas no XML. Até aqui, tudo parece só verboso. A parte que trava o deploy é a das colunas.
A armadilha que trava o deploy: a coluna não é o nome do campo
O erro que mais custa tempo aqui é assumir que <field> aceita o API name do campo. Não
aceita. O metadado de Report usa tokens do report type, e eles quase nunca coincidem com o
nome do campo. O nome do Owner não é OwnerId, é FULL_NAME. O valor da Oportunidade
não é Amount, é AMOUNT. O estágio é STAGE_NAME, a data de fechamento é
CLOSE_DATE, o filtro de ganha usa WON com valor true. Só o campo
custom aparece com o nome totalmente qualificado, como Opportunity.Discount_Percent__c. Chutar o
token é garantia de deploy falhado com um "no CustomField named ... found" pouco explicativo.
Em vez de descobrir por tentativa e erro, descreva o report type pela Analytics REST API e leia os tokens reais. Um GET autenticado no describe do tipo devolve cada coluna com o seu token e o campo que ela representa:
GET /services/data/v60.0/analytics/report-types/OpportunityProduct
Authorization: Bearer <session id>
// resposta (recortada): cada coluna traz o token e o entityColumnName
"FULL_NAME": { "entityColumnName": "User.Name" }
"AMOUNT": { "entityColumnName": "Opportunity.Amount" }
"OPPORTUNITY_NAME": { "entityColumnName": "Opportunity.Name" }
O que está à esquerda, o token, é o que vai dentro de <field>. O que está à direita é só a
pista de qual campo aquele token representa. Cinco minutos lendo o describe evitam uma tarde inteira de deploy
recusado, e o mesmo endpoint funciona para descobrir os tokens de qualquer report type, inclusive os de
objeto custom, onde o nome do owner muda de FULL_NAME para CUST_OWNER_NAME. Se você
está começando no CLI agora, o caminho de baixar e subir esses arquivos é o mesmo do
sf CLI para quem ainda usa Change Sets.
O dashboard que aponta para os relatórios
O dashboard é o segundo arquivo, e ele não guarda dado nenhum: guarda referências aos relatórios e a forma de
cada componente. O .dashboard-meta.xml declara o usuário de execução, o título, e a lista de
componentes, cada um apontando para um relatório pela pasta e pelo nome de desenvolvedor. O ponto delicado é
que a ordem dos componentes no arquivo segue a ordem alfabética do schema, não a ordem que você vê na tela, e
alguns limites mordem em silêncio: o <title> de um componente aceita no máximo 40
caracteres, e não existe threshold numérico de cor por metadado. Um gauge que fica vermelho acima de um valor
não se configura com um "breakpoint", e sim com a matemática dos terços do próprio gauge.
Vale guardar esse detalhe, porque foi onde tropecei: para um medidor virar vermelho a partir de 15 por cento,
o gaugeMax precisa ser 22,5, já que a faixa vermelha padrão começa no último terço da escala.
Cor por metadado de dashboard é sempre indireta assim. A lógica de por que aquele KPI existe e o que ele
mede eu tratei no post sobre o
dashboard de KPIs configurável via Custom Metadata;
aqui o foco é só levar o painel pronto para o repositório sem que o deploy recuse.
Trazer o painel inteiro para o repositório
Com os tokens certos, o retrieve é a parte rápida. O detalhe que passa despercebido é que a pasta também é
metadado: sem trazer o ReportFolder e o DashboardFolder, o deploy no destino falha
porque o relatório aponta para uma pasta que não existe lá. O comando completo puxa os quatro:
sf project retrieve start \
--metadata \
"Report:Comercial/Comissao_por_Vendedor" \
"Dashboard:Comercial/Painel_do_Gestor" \
"ReportFolder:Comercial" \
"DashboardFolder:Comercial" \
--target-org producao
O CLI escreve os arquivos na estrutura do projeto: o relatório em
reports/Comercial/Comissao_por_Vendedor.report-meta.xml e o dashboard em
dashboards/Comercial/Painel_do_Gestor.dashboard-meta.xml, com a pasta virando seu próprio
arquivo .reportFolder-meta.xml. A partir daqui é o fluxo de sempre: git add,
commit, e o painel passa a ter dono no histórico. Deploy de volta para uma sandbox ou para produção é o
sf project deploy start apontando a mesma pasta, e a sandbox finalmente reflete a produção sem
alguém redigitar filtro na interface.
Org sozinha contra painel versionado
A diferença não é conforto de desenvolvedor, é o que você consegue responder quando o número quebra numa terça-feira. A tabela deixa explícito o que cada modelo entrega no momento em que dá problema:
| Na hora do problema | Só na org | Versionado no Git |
|---|---|---|
| Quem mudou o filtro | ninguém sabe | git blame na linha do XML |
| Voltar ao estado anterior | recriar de memória | git revert e deploy |
| Revisar antes de subir | impossível | pull request no diff do metadado |
| Sandbox igual à produção | diverge em silêncio | mesmo deploy nos dois |
| Auditoria da mudança | print de tela | histórico completo do commit |
Nenhuma dessas linhas depende de uma ferramenta paga ou de um pipeline de DevOps sofisticado. Todas caem no colo assim que o arquivo do painel vive no mesmo repositório do resto do projeto. É o ganho que o Change Set nunca deu, porque ele trata relatório e dashboard como caixa-preta que você move sem enxergar por dentro.
O caso da Vetra: o dia que o git diff achou o filtro
Depois que levei o painel do gestor da Vetra para o repositório, o desfecho do incidente que abriu este post
mudou de figura. Quando a comissão voltou a divergir do relatório, não houve arqueologia: um
git log na pasta reports/Comercial/ mostrou o commit que alterou o filtro, o
git diff mostrou que o critério de estágio tinha sido trocado, e o git revert
devolveu o relatório ao estado correto num deploy. O que antes era uma tarde de investigação virou três
comandos.
Esse mesmo painel me deu a cicatriz mais útil do processo. Eu quis ordenar os vendedores pela média de
desconto que cada um concedia, e o token documentado para média de campo custom,
a!Opportunity.Discount_Percent__c, foi rejeitado no deploy desta org com um "no CustomField
named" enganoso. A saída que passou pelo schema foi criar uma custom summary formula no próprio relatório,
com o cálculo AMOUNT:SUM*Opportunity.Discount_Percent__c:AVG/100, e ordenar o agrupamento por
ela. É o tipo de detalhe que só aparece quando você escreve o metadado à mão, e que fica registrado no
repositório para o próximo relatório do mesmo tipo nascer certo. A regra de comissão que esse painel exibe é
a mesma engine que descrevi no post sobre
comissão no Salesforce sem planilha: o
relatório versionado é a janela auditável para o número que a engine calcula.
Quando não vale versionar um relatório
Honestidade acima de dogma: nem todo relatório merece um lugar no Git, e insistir nisso vira ruído. O relatório ad-hoc que um analista monta na sexta para responder uma pergunta pontual e nunca mais abre não precisa de versionamento; jogá-lo no repositório só enche o histórico de arquivos que ninguém vai revisar. O alvo é o painel operacional, o relatório que alimenta comissão, meta ou reunião de resultado, e os dashboards oficiais que mais de uma pessoa tem permissão de editar. É nesses que a edição silenciosa custa caro e o histórico salva.
Vale também medir a dor antes de montar o processo. Se a sua operação tem cinco relatórios e ninguém nunca mexe neles, o retorno de versioná-los é pequeno, e tudo bem deixá-los na org. O custo de trazer analytics para o repositório se paga quando existem muitos painéis e várias mãos editando, que é justamente o cenário em que um filtro trocado sem aviso derruba a confiança no número. Comece pelos painéis que a diretoria olha, versione esses primeiro, e deixe a cauda de relatórios pessoais onde ela está. O objetivo não é versionar tudo, é tirar do escuro o que sustenta decisão.
Seus dashboards vivem só na org, sem histórico de quem mudou o quê?
Eu trago a sua camada de analytics para o repositório: recupero os relatórios e dashboards operacionais como metadado, acerto os tokens que travam o deploy, e deixo o painel com diff e rollback no mesmo pipeline do resto do projeto. Comece com um diagnóstico gratuito de 45 minutos.
Falar no WhatsApp Ver serviçosPerguntas frequentes
Dá para versionar Relatório e Dashboard do Salesforce no Git?
Dá. Relatório e Dashboard são metadado como qualquer outro: você recupera cada um como
.report-meta.xml e .dashboard-meta.xml com o sf CLI, comita nas pastas
reports/ e dashboards/ do repositório e passa a fazer deploy pelo mesmo pipeline do
Apex. A partir daí o painel tem histórico, diff e rollback, coisas que a edição direta na org não oferece.
Por que o nome da coluna no .report-meta.xml não é o nome do campo?
Porque o metadado de Report usa tokens do report type, não os API names dos campos. O nome do Owner vira
FULL_NAME, o valor da Oportunidade vira AMOUNT, e só o campo custom aparece com o
nome qualificado, como Opportunity.Discount_Percent__c. Você descobre o token correto de cada
coluna descrevendo o report type pela Analytics REST API antes de escrever o XML.
Como recuperar um Relatório existente para o repositório?
Com sf project retrieve start passando Report e a pasta dele no formato
Pasta/NomeDoRelatorio, mais o ReportFolder correspondente, porque a pasta também é
metadado. O CLI escreve o arquivo em reports/Pasta/Nome.report-meta.xml na estrutura do
projeto, pronto para o git add.
Preciso versionar todos os relatórios da org?
Não, e nem vale. Relatório ad-hoc que um usuário cria para responder uma dúvida do dia e descarta não precisa ir para o Git; isso só polui o repositório. Versione os relatórios canônicos, os que alimentam decisão operacional, e os dashboards oficiais que várias pessoas editam. É onde o histórico e o rollback realmente salvam o dia.
