Saltar al contenido
BeryX
Volver a las respuestas para RFP

Documentación de BeryX

Este documento técnico está disponible en portugués.

Integração com orquestradores e integradores (dbt Cloud, dbt Core, Airflow, Fivetran, Airbyte)

O BeryX liga-se aos orquestradores de duas formas complementares (e lê a saúde dos pipelines dos integradores Fivetran e Airbyte — §3):

dbt Cloud Airflow
Como Webhook do dbt Cloud → POST /api/v1/webhooks/dbt-cloud/{id} Pacote beryx-airflow (operator, sensor, callbacks)
Dispara scan Automaticamente quando um job run conclui, nos datasets do projeto mapeado BeryXScanOperator numa DAG, espera pelo veredito
Contexto no RCA Cada run (sucesso/erro/cancelado) fica guardado Os callbacks reportam o estado das tasks
Autenticação Assinatura HMAC-SHA256 do dbt Cloud Token de API dvk_*

Com isto, o diagnóstico de causa-raiz (RCA) de uma issue passa a dizer, por exemplo: «o job dbt «Daily Job (dbt build)» falhou 20 min antes desta issue» — tanto no relatório IA como no template offline.

Com o dbt Core (§4), o caminho é o inverso: o dbt test corre no teu pipeline, no teu warehouse, e devolve os resultados ao BeryX — as regras exportadas como testes dbt voltam como resultados dessas regras.


1. dbt Cloud — webhook «job run concluído»

1.1 Criar o webhook no dbt Cloud

  1. No dbt Cloud: Account settings → Webhooks → Create webhook.
  2. Eventos: Run completed (obrigatório — é o que dispara o scan). Opcionalmente Run started e Run errored, para o contexto do RCA ficar mais rico.
  3. Jobs: os que carregam as tabelas monitorizadas pelo BeryX.
  4. Endpoint: ainda não o tens — primeiro configura o BeryX (1.2) e depois volta aqui com o URL. O dbt Cloud mostra o Secret do webhook ao guardar: copia-o.

1.2 Configurar o BeryX (admin)

curl -X PUT https://api.praxis.lbostuff.tech/api/v1/integrations/dbt-cloud \
  -H "Authorization: Bearer $JWT_ADMIN" -H "Content-Type: application/json" \
  -d '{
        "secret": "<Secret do webhook dbt Cloud>",
        "account_id": "<id da conta dbt Cloud (opcional, recusa eventos de outras contas)>",
        "project_mappings": [
          {"project_id": "167194", "connection_id": "<id da conexão BeryX>"},
          {"project_id": "170001", "connection_id": "<id>", "dataset_ids": ["<id>", "<id>"]}
        ]
      }'

A resposta traz o webhook_path (/api/v1/webhooks/dbt-cloud/<id>) — o URL completo a colar no dbt Cloud é https://api.praxis.lbostuff.tech + esse path.

  • Mapeamento projeto dbt → conexão: cada project_id do dbt Cloud aponta para uma conexão do workspace; dataset_ids (opcional) limita o scan a esses datasets — sem ele, o scan cobre a conexão inteira.
  • Segredo: cifrado com Fernet antes de persistir; nunca é devolvido pela API (has_secret: true) nem escrito nos logs. PUT sem secret mantém o atual; para rodar, envia o novo segredo.
  • GET /integrations/dbt-cloud mostra a configuração e last_delivery_at; DELETE remove-a. Alterações ficam na auditoria (dbt_cloud_configured).

1.3 O que acontece em cada evento

Evento dbt Cloud Efeito
job.run.started Run guardado como running
job.run.errored Run guardado como error (hora do erro em runErroredAt)
job.run.completed Run guardado com o estado final (success/error/cancelled) e pedido de scan (trigger = dbt_cloud) dos datasets do projeto mapeado
outros Aceites e ignorados (status: ignored)
  • Verificação da assinatura: o header Authorization tem de ser o HMAC-SHA256 (hex) do corpo cru com o segredo. Assinatura errada → 401; integração inexistente ou desligada → 404.
  • Idempotência: re-entregas atualizam o mesmo run (workspace + run_id) e só o primeiro completed dispara scan (scan: already_requested). Um started que chegue atrasado não faz um run terminado voltar a running.
  • O scan corre mesmo quando o job dbt falhou: tabelas por atualizar aparecem como issues de frescura/volume, com o run falhado no RCA.
  • Nunca falha a entrega por causa do scan: projeto não mapeado (unmapped_project), quota do plano esgotada (plan_limit), scans pedidos em fila no máximo do plano (backpressure, CAPACIDADE.md §3) ou mensageria em baixo (messaging_unavailable) → 200 com o motivo em scan; o run fica guardado como contexto na mesma. Com 200 o dbt Cloud não reenvia, por isso o scan desse run não é repetido: pede-o o próximo run concluído do job, o scan agendado da conexão ou um pedido manual.
  • Dados guardados: ids e nomes de job/projeto/ambiente, estado, mensagem de estado (≤ 500 caracteres) e horas. O runReason (que traz o email de quem lançou o run) é descartado.

1.4 Testar localmente

BODY='{"eventType":"job.run.completed","accountId":1,"data":{"runId":"1","projectId":"167194","jobName":"teste","runStatus":"Success","runStatusCode":10}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SEGREDO" -hex | sed 's/^.* //')
curl -X POST http://localhost:8000/api/v1/webhooks/dbt-cloud/<id> \
  -H "Authorization: $SIG" -H "Content-Type: application/json" -d "$BODY"

1.5 Runs e freshness declarada vs observada (Discovery API)

O webhook diz quando um job acaba. A Discovery API do dbt Cloud diz o que o dbt acha de cada tabela: o último run de cada modelo e o resultado da dbt source freshness (critérios warn_after/error_after declarados). O BeryX cruza isso com a frescura que observa na fonte (última modificação da tabela, recolhida em cada scan pelos monitores automáticos) e abre um issue quando os dois discordam — «o dbt diz fresh, os dados dizem 26h».

  1. No dbt Cloud: Account settings → Service tokens → New token, permissão Metadata Only (só leitura de metadados).
  2. Descobre o environment_id do ambiente de produção (Deploy → Environments, o número no URL).
  3. Acrescenta à configuração:
curl -X PUT https://api.praxis.lbostuff.tech/api/v1/integrations/dbt-cloud \
  -H "Authorization: Bearer $JWT_ADMIN" -H "Content-Type: application/json" \
  -d '{
        "api_token": "<service token Metadata Only>",
        "metadata_api_url": "https://metadata.cloud.getdbt.com/graphql",
        "project_mappings": [
          {"project_id": "167194", "connection_id": "<id>", "environment_id": "218762"}
        ]
      }'
  • metadata_api_url é opcional (por omissão o multi-tenant US). Contas EMEA/AU ou cell-based usam o URL que o dbt Cloud mostra em Account settings → Access URLs (ex.: https://ab123.metadata.us1.dbt.com/graphql). Só hosts *.getdbt.com/*.dbt.com, sempre https.
  • O token é cifrado (Fernet) e nunca devolvido (has_api_token: true). PUT sem api_token mantém-no; "api_token": "" remove-o.
  • PUT sem metadata_api_url mantém o URL guardado; "metadata_api_url": "" volta ao multi-tenant US. O token guardado só segue para o host com que foi guardado: mudar o URL exige o api_token no mesmo pedido (ou "" para o remover), senão a resposta é 400.
  • Quando corre: de hora a hora no worker, ou já com POST /integrations/dbt-cloud/metadata-sync (admin). Erros do dbt Cloud (token recusado, ambiente inexistente) ficam em metadata_sync_error.

O que é comparado (sempre no instante do check/run do dbt, nunca extrapolado):

Divergência Quando Severidade
fresh_but_stale — «dbt diz fresh, os dados dizem 26h» Source freshness pass, mas a tabela não é modificada há mais do que o critério declarado (o loaded_at_field não reflete as cargas, ou a carga não escreveu nada) aviso
stale_but_fresh Source freshness warn/error, mas a tabela foi modificada dentro do critério info
run_not_reflected Último run de um modelo table com sucesso, mas a tabela não é modificada desde antes do run aviso
  • Os nós do dbt casam com os datasets da conexão mapeada por schema + alias/identifier.
  • Sem critério declarado, sem observação posterior ao check/run, modelos view/incremental ou runs falhados → not_comparable (com o motivo); os runs falhados já entram no RCA pelo webhook.
  • Um issue dbt_divergence por dataset e tipo: é atualizado a cada sync, resolve-se sozinho quando os dois lados voltam a concordar e fica calado se for marcado «ignorado».
  • GET /integrations/dbt-cloud/node-states?verdict=divergent lista o estado por dataset: último run, freshness declarada, frescura observada e veredito.
  • Só metadados: nunca SQL compilado, logs de runs nem linhas.

2. Airflow — pacote beryx-airflow

Código em integrations/beryx_airflow/ (publica-se no PyPI pelo sdk-publish.yml com package=airflow — sdk/PUBLISHING.md; ver o README do pacote). Airflow 2.6+ e 3.x; o cliente HTTP usa só a stdlib.

2.1 Ligação

Cria um token de API na página Equipa e uma Connection no Airflow:

airflow connections add beryx_default \
  --conn-type beryx \
  --conn-host https://api.praxis.lbostuff.tech \
  --conn-password "$DATAVAULT_TOKEN"     # dvk_… — guardado no secrets backend

Os tokens dvk_* são de leitura, com três exceções explícitas: POST /rag-gate, POST /scans (sujeito à quota) e POST /orchestrator-runs.

2.2 Gate de qualidade numa DAG

from beryx_airflow import BeryXScanOperator, report_to_beryx

report = report_to_beryx(connection_id="<id da conexão BeryX>")

with DAG(
    "orders_daily",
    default_args={"on_failure_callback": report, "on_success_callback": report},
    ...
):
    quality_gate = BeryXScanOperator(
        task_id="beryx_quality_gate",
        connection_id="<id da conexão BeryX>",
        dataset_ids=["<id fct_orders>"],   # omitido => todos os datasets da conexão
        fail_on="fail",                    # "warn" chumba também em warnings
    )
    load >> transform >> quality_gate >> publish
  • O operator dispara o scan, espera que termine (poll_interval, scan_timeout) e lê GET /datasets/{id}/verdict de cada dataset. O pior veredito ganha; a task falha em fail (ou warn, com fail_on="warn") e quando o scan falha.
  • XCom return_value: {scan_id, scan_status, verdict, datasets[]}.
  • Para não ocupar um worker durante o scan: wait_for_completion=False + BeryXScanSensor(scan_id=..., mode="reschedule").
  • report_to_beryx é best-effort (nunca falha a DAG) e envia só metadados: dag/task/run, estado, horas e a classe da exceção — nunca a mensagem.

3. Integradores — Fivetran e Airbyte (saúde de pipelines)

O dbt e o Airflow transformam; o Fivetran e o Airbyte trazem os dados da origem para o destino. O BeryX lê — só leitura, pelas APIs oficiais — o que o integrador sabe de cada pipeline (último sync, estado, falhas, cadência, tabelas que escreve) e junta-lhe o que observa nas duas pontas:

  • estado por dataset de destino: ok, failed (o último sync falhou), delayed (sem sucesso há mais do que o tolerado), paused ou unknown;
  • latência origem→destino: «origem atualizada há 20 min, destino há 5h» — e lag_s, quanto o destino vai atrás da origem;
  • issues pipeline_sync (failed/delayed), um por dataset e subtipo;
  • contexto no RCA: cada sync fica em orchestrator_runs (source fivetran ou airbyte), ligado à conexão de destino — o diagnóstico passa a dizer «o sync Fivetran «salesforce» falhou 40 min antes desta issue»;
  • reconciliação automática origem↔destino (RECON v1) quando a conexão de origem também está ligada ao BeryX e as duas pontas têm perfil.

3.1 Credenciais (só leitura)

Fivetran Airbyte
Credencial Chave + segredo da API (Account settings → API key) de um utilizador ou papel com leitura das connections Client id + client secret de uma application (Settings → Applications)
Host https://api.fivetran.com (fixo) Airbyte Cloud https://api.airbyte.com (omissão) ou o URL da instância self-managed, ex. https://airbyte.empresa.pt/api/public
Âmbito opcional (scope_id) group_id (destino Fivetran) workspace_id do Airbyte
Chamadas GET /v1/connections (paginado por cursor), GET /v1/connections/{id}/schemas POST /v1/applications/token (troca de credenciais por token — o único POST, numa allowlist fechada), GET /v1/connections, GET /v1/jobs?connectionId=…&jobType=sync
  • Nenhum pedido altera o integrador: não lançamos syncs, não mudamos schemas nem agendas. Todos os GET passam pelo cliente HTTP read-only com o guard anti-SSRF; um POST fora da allowlist é recusado antes de tocar na rede.
  • As credenciais são cifradas (Fernet) e nunca devolvidas nem escritas nos logs; as mensagens de erro do integrador são cortadas a 500 caracteres.
  • O URL do Airbyte self-managed tem de ser https; metadata da cloud é sempre bloqueada e redes privadas só onde o ambiente as permite (e com o host em DATAVAULT_CONNECTOR_ALLOWED_HOSTS).
  • Mudar o URL (incluindo Cloud ↔ self-managed) exige reenviar client_id e client_secret no mesmo PATCH (senão 400): as credenciais guardadas nunca seguem para um host novo. A auditoria regista o URL antigo e o novo.
  • Cada sync tem um orçamento de 300 pedidos, retries limitados em 429/5xx (Retry-After até 10 s) e no máximo 500 pipelines; o que ficar por ler vem no próximo (status: partial).

3.2 Configurar (admin)

curl -X POST https://api.praxis.lbostuff.tech/api/v1/integrations/pipelines/fivetran \
  -H "Authorization: Bearer $JWT_ADMIN" -H "Content-Type: application/json" \
  -d '{
        "name": "Fivetran produção",
        "credentials": {"api_key": "<chave>", "api_secret": "<segredo>"},
        "scope_id": "<group_id (opcional)>",
        "destination_connection_id": "<id da conexão do warehouse no BeryX>",
        "pipeline_mappings": [
          {"pipeline_id": "<id da connection Fivetran>",
           "source_connection_id": "<id da conexão de origem, se o BeryX a monitoriza>",
           "max_delay_minutes": 180}
        ]
      }'

Para o Airbyte, POST /integrations/pipelines/airbyte com "credentials": {"client_id": "…", "client_secret": "…"} e, se for self-managed, "base_url".

  • Destino: destination_connection_id é a conexão para onde escrevem os pipelines por omissão; um mapeamento pode indicar outra (destination_connection_id) e o schema de destino (destination_schema) quando o integrador não o diz (Airbyte com namespace do destino).
  • Origem (opcional): com source_connection_id, a tabela de origem casa com o dataset da conexão de origem — latência e reconciliação.
  • Atraso tolerado: por omissão 2 × a cadência declarada + 15 min (Fivetran sync_frequency; Airbyte agenda basic). Pipelines com agenda cron/manual só ficam «atrasados» com max_delay_minutes (5 min–7 dias) ou quando o próprio Fivetran os marca atrasados.
  • GET /integrations/pipelines lista as integrações com o retrato do último sync por pipeline; PATCH/DELETE …/{provider}/{id} alteram/removem (remover resolve os issues que a integração abriu); POST …/test valida as credenciais; POST …/sync sincroniza já (o worker fá-lo de hora a hora, e uma falha inesperada numa integração não trava as seguintes). Tudo na auditoria (pipeline_integration_*), sem credenciais.

3.3 O que é comparado

  • Casamento: tabela do integrador (schema + nome no destino) ↔ dataset da conexão de destino, sem distinguir maiúsculas (o Snowflake põe tudo em maiúsculas); sem schema conhecido, só pelo nome e só se for único.
  • Falhado: a última falha é mais recente do que o último sucesso — a mensagem do integrador (task do Fivetran) vai para o issue e para o RCA. A API pública do Airbyte não expõe a mensagem de erro de um job.
  • Atrasado: sem sucesso há mais do que o tolerado.
  • Latência: source.updated_at = última modificação observada na origem (monitores automáticos); destination.updated_at = último sync bem-sucedido.
  • Reconciliação: reconcile_counts entre os perfis mais recentes das duas pontas (tolerância 1 %). comparable: true só quando a origem foi contada antes do último sync e o destino depois; senão a diferença pode ser só dados novos. Não abre issue. As linhas que o integrador diz ter sincronizado são incrementais e nunca entram na conta.
  • Mudanças de schema: tabelas que entraram ou saíram do pipeline entre dois syncs (schema_change no retrato do pipeline e na saúde do dataset).
  • Um issue pipeline_sync por dataset e subtipo: atualizado a cada sync, resolve-se sozinho quando o sync volta ao normal e fica calado se for marcado «ignorado». Severidade aviso; dimensão atualidade (delayed) ou disponibilidade (failed).
  • Volume: o Fivetran e o Airbyte não dão linhas por tabela nas APIs REST públicas; o Airbyte dá linhas por sync (pipeline_rows_synced).

GET /datasets/{id}/pipeline-health devolve tudo isto por dataset, com os syncs recentes. ADF e Talend são as fatias seguintes (mesmo contrato PipelineMetadataConnector).


4. dbt Core — resultados do dbt test (run_results.json)

O BeryX exporta as regras como testes dbt (GET /datasets/{id}/rules.dbt.yml e, para as regras SQL, rules.dbt-tests.zip). Cada teste exportado leva a regra de origem em config.meta.beryx.rule_id. Os testes exportados antes da API 1.77.0 têm config.meta.datavault.rule_id, que continua a casar até 31 de maio de 2027. Quando o teu pipeline corre dbt test ou dbt build, envia o target/run_results.json e o target/manifest.json a POST /api/v1/integrations/dbt/run-results. Os resultados aparecem onde geres a qualidade: no veredito do gate de CI, nos issues e nas atividades. O round-trip deixa de ser só de definições.

4.1 Enviar os resultados

Com o SDK (pip install beryx-sdk) e um token de API dvk_*:

dbt build                                   # ou dbt test
DATAVAULT_TOKEN=dvk_... DATAVAULT_API_URL=https://api.exemplo.pt \
  beryx dbt push-results --run-results target/run_results.json

Sem --manifest, o comando usa o manifest.json da mesma pasta. Com --connection <id>, só casa com os datasets dessa conexão. Sai com 0 quando envia (ou quando aquela invocação já tinha sido enviada) e com 2 em erro.

Passo de CI (GitHub Actions; o mesmo em GitLab/Jenkins):

- name: dbt build
  run: dbt build --target prod
- name: Resultados para o BeryX
  if: always()          # envia também quando há testes a falhar
  env:
    DATAVAULT_TOKEN: ${{ secrets.DATAVAULT_TOKEN }}
    DATAVAULT_API_URL: ${{ vars.DATAVAULT_API_URL }}
  run: beryx dbt push-results --run-results target/run_results.json

Hook on-run-end (dbt_project.yml). O dbt só escreve o run_results.json depois dos hooks, por isso o hook não o pode enviar. Usa o passo a seguir ao comando, como acima, ou um wrapper:

dbt test; status=$?
beryx dbt push-results --run-results target/run_results.json || true
exit $status

Sem o SDK, o corpo é um objeto JSON com os dois ficheiros:

jq -n --slurpfile r target/run_results.json --slurpfile m target/manifest.json \
  '{run_results: $r[0], manifest: $m[0]}' |
curl -sf -X POST "$DATAVAULT_API_URL/api/v1/integrations/dbt/run-results" \
  -H "Authorization: Bearer $DATAVAULT_TOKEN" -H "Content-Type: application/json" -d @-

4.2 Como cada teste é lido

  1. Dataset: o nó testado casa por schema + alias/identifier, sem distinguir maiúsculas, como no §1.5. Um schema diferente do catálogo não casa pelo nome. Só casa com os datasets em que o utilizador do token (ou da sessão) pode escrever: com domínios restritos, um dataset fora do teu acesso, ou só com leitura, fica como no_dataset, igual a um que não existe. Nada é gravado nele e a resposta não traz o dataset_id nem o rule_id.
  2. Regra: primeiro pelo rule_id em config.meta.beryx ou, até 31 de maio de 2027, em config.meta.datavault. Com as duas, vale a beryx. Esse id só conta se a regra for do teu workspace, estiver ativa e for do dataset casado. Depois, pela assinatura: coluna + tipo de teste mapeado para o tipo de regra (not_null, unique, accepted_values, regex, intervalo, validade), a mesma do GAP do import do manifest.
  3. Com regra: um resultado dessa regra (engine: dbt). Conta para GET /datasets/{id}/verdict: uma regra crítica a falhar no dbt dá fail, e o envio seguinte com pass limpa-o. O veredito é o do último scan do BeryX, com o resultado dbt mais recente de cada regra por cima. Um dbt test com uma só regra não esconde as falhas do scan. Uma falha abre ou atualiza o mesmo issue rule_violation que o scan abriria. Um pass resolve-o se a última evidência veio do dbt.
  4. Sem regra equivalente (testes singulares, relationships, testes de pacotes sem mapeamento): issue rule_violation com subtype dbt_test e o nome do teste. fail/error → crítico, warn → aviso, pass resolve. Um issue ignorado silencia esse teste.

Testes skipped, nós que não são testes (models de um dbt build) e testes que não estão no manifest enviado são contados no relatório e não mexem em nada.

4.3 Garantias

  • Idempotente: o invocation_id fica como um run dbt_core em GET /orchestrator-runs (contexto do RCA). Reenviar a mesma invocação devolve status: duplicate e não grava nada.
  • Só metadados de execução: o schema é fechado (as chaves do run_results.json v4 a v6, dbt 1.3 a 1.10). Uma chave desconhecida, um campo com linhas (failed_rows, rows, …) ou um failures que não seja uma contagem dão 422. O SQL compilado, as mensagens, o adapter_response e os args servem para validar e não são gravados nem logados. Com store_failures, as linhas ficam no teu warehouse.
  • Nada é lido da fonte: os resultados entram num scan dbt sintético por conexão, que não conta para a quota mensal de scans do plano.
  • Teto: 60 MB por pedido (o manifest vai junto). Nesta versão, o manifest é obrigatório.

5. Onde aparece

  • RCA (POST /issues/{id}/diagnose): o contexto ganha orchestrator_runs — runs da conexão do dataset nas 24 h antes da issue (até 5, mais recentes primeiro), com minutes_before_issue e a frase summary. Um run falhado é a primeira hipótese do template offline («Job de orquestração falhado a montante»).
  • API: GET /orchestrator-runs?connection_id=&source=&status= (source dbt_cloud, airflow, fivetran, airbyte ou dbt_core) e GET /datasets/{id}/pipeline-health (§3).

Arquitetura: docs/ARQUITETURA.md §3.32 e §3.49.


6. Ferramentas de BI — Power BI, Tableau e Looker

O BeryX lê — só leitura, pelas APIs oficiais — o que a ferramenta de BI sabe dos seus datasets BI (modelo semântico do Power BI, fonte de dados publicada do Tableau, modelo LookML do Looker) e dos dashboards que os usam, e junta-lhe o que já sabe das tabelas:

  • issues bi_refresh (failed/delayed), um por dataset BI e subtipo, preso ao dataset do BeryX de que ele depende: «Refresh Tableau falhado: Vendas», com os dashboards que ficam com dados desatualizados. Resolvem-se sozinhos quando o refresh volta ao normal e ficam calados se marcados «ignorado». Um refresh agendado está atrasado quando o último sucesso tem mais do que o maior intervalo da agenda
    • 1 h; sem agenda (ligação viva, refresh manual) nunca está atrasado;
  • linhagem tabela → dataset BI → dashboard: cada dashboard ligado a pelo menos um dataset do BeryX entra na linhagem como exposure, ao lado dos exposures dbt. O impacto de uma issue, o RCA, o grafo do conhecimento e os consumidores do data product passam a citar o dashboard e o seu dono;
  • aviso de dashboards sem visualizações há N dias (unused_after_days, 30 por omissão), no retrato da integração. Só com uso conhecido: sem permissão para ler a atividade, nenhum dashboard é dado como «sem uso». No Power BI, N vai de 7 a 27 e é 27 por omissão: a API de atividade só serve os últimos 28 dias e o dia de hoje ainda vai a meio, por isso cobre 27 dias completos. Um N acima disso dá 400.

6.1 Credenciais (só leitura)

Ferramenta Credenciais base_url scope_id
Power BI tenant_id, client_id, client_secret de um service principal do Entra ID com acesso de leitura aos workspaces; para o uso, a definição de inquilino que deixa service principals usar as APIs de administração só de leitura — (hosts fixos) workspace do Power BI (opcional)
Tableau token_name, token_secret de um personal access token servidor (Tableau Cloud ou Server) site (contentUrl)
Looker client_id, client_secret das API keys de um utilizador com leitura instância Looker —

Ficam cifradas (Fernet) e nunca voltam na API. Mudar o URL ou o âmbito obriga a reenviar as credenciais no mesmo pedido.

6.2 Configurar (admin)

curl -X POST $API/integrations/bi/tableau -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{
    "name": "Tableau Cloud",
    "base_url": "https://10ax.online.tableau.com",
    "scope_id": "vendas",
    "credentials": {"token_name": "datavault", "token_secret": "…"},
    "warehouse_connection_id": "<conexão do armazém>",
    "unused_after_days": 30
  }'
curl -X POST $API/integrations/bi/tableau/<id>/test -H "Authorization: Bearer $TOKEN"
curl -X POST $API/integrations/bi/tableau/<id>/sync -H "Authorization: Bearer $TOKEN"

As tabelas que a ferramenta lê casam com os datasets da conexão do armazém (warehouse_connection_id; sem ela, com os do workspace, só quando o nome é único), por schema + tabela sem distinguir maiúsculas. O Power BI não diz as tabelas de um modelo semântico na API REST pública: liga-o à mão com "asset_mappings": [{"asset_id": "<id do modelo>", "dataset_ids": ["<dataset>"]}]. O worker repete o sync de hora a hora; GET /integrations/bi mostra o retrato do último (datasets BI com estado, dashboards com uso, avisos e o que ficou por ler).

6.3 Limites de um sync

Cada sync tem um orçamento de 600 pedidos, com retries limitados em 429/5xx.

  • Tableau e Looker cabem no orçamento com os tetos de páginas (Tableau) e de detalhes, explores e donos (Looker: 300, 150 e 100). O Tableau cabe mesmo com todos os retries (189 pedidos no pior caso). O Looker gasta até 553 pedidos sem retries: com muitos 429/5xx pode passar os 600, e o corte segue a regra abaixo.
  • Power BI gasta cerca de 2 + 3 × workspaces + 2 × modelos com refresh + dashboards + 2 × (N + 1) pedidos de atividade. Sem scope_id, uma conta com dezenas de workspaces passa os 600.
  • Orçamento esgotado a meio: o sync fica partial (POST …/sync responde 200 com partial_code: request_budget) e grava o que leu por inteiro. As issues e os dashboards na linhagem que ficaram por ler não mudam: nada é resolvido nem apagado. Um dashboard lido só ganha ligações, e a issue de um dataset BI que só perdeu as tabelas casadas fica como estava, porque as ligações podem ter vindo cortadas. O sync seguinte volta a parar no mesmo ponto enquanto a conta tiver esse tamanho; no Power BI, a nota sugere uma integração por workspace (scope_id).
  • Resultados parciais do Tableau: os avisos NODE_LIMIT_EXCEEDED, LINKED_RESULTS_INCOMPLETE e MAX_PAGE_SIZE_EXCEEDED da Metadata API trazem dados parciais. O sync fica partial (partial_code: partial_results), com a mesma regra.
  • Refreshes por ler (tarefas do Tableau ou datagroups do Looker sem permissão): o dataset BI fica unknown e a sua issue de refresh fica como estava.
  • Uso do Tableau: lê-se até 20 páginas de 1000 vistas. As vistas não vêm por livro, e o aviso compara totais entre syncs. Num site com mais de 20 000 vistas, ou com um erro ou o orçamento a meio das páginas, o uso de todos os livros fica desconhecido e nenhum sai no aviso.

Arquitetura: docs/ARQUITETURA.md §3.59.