DevOps

Versionar Relatório e Dashboard do Salesforce no Git

Banner do artigo sobre versionar relatório e dashboard do Salesforce no Git como metadado

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 problemaSó na orgVersionado no Git
Quem mudou o filtroninguém sabegit blame na linha do XML
Voltar ao estado anteriorrecriar de memóriagit revert e deploy
Revisar antes de subirimpossívelpull request no diff do metadado
Sandbox igual à produçãodiverge em silênciomesmo deploy nos dois
Auditoria da mudançaprint de telahistó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ços

Perguntas 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.