Ir para o conteúdo principal

Plugin de Analytics

Plugin de Analytics

Permite executar queries SQL em SQL Warehouses do Databricks.

Principais recursos:

  • Queries SQL baseadas em arquivos, com geração automática de tipos
  • Queries parametrizadas com SQL helpers type-safe
  • Suporte aos formatos JSON e Arrow
  • Cache e lógica de retry integrados
  • Streaming via Server-Sent Events (SSE)

Uso básico

import { analytics, createApp, server } from "@databricks/appkit";

await createApp({
  plugins: [server(), analytics({})],
});

Arquivos de query

  • Coloque os arquivos .sql em config/queries/
  • A chave da query é o nome do arquivo sem .sql (ex.: spend_summary.sql"spend_summary")

Contexto de execução

  • queryKey.sql é executado como service principal (cache compartilhado)
  • queryKey.obo.sql é executado como usuário (OBO = on-behalf-of, cache por usuário)

O contexto de execução é determinado pelo nome do arquivo SQL, e não pela chamada do hook.

Parâmetros SQL

Use marcadores :paramName e, opcionalmente, anote os tipos dos parâmetros com comentários SQL:

-- @param startDate DATE
-- @param endDate DATE
-- @param limit INT
SELECT ...
WHERE usage_date BETWEEN :startDate AND :endDate
LIMIT :limit

LIMIT / OFFSET exigem especificamente o IntegerType do Spark — BIGINT (LongType) é rejeitado com INVALID_LIMIT_LIKE_EXPRESSION.DATA_TYPE. Anote com INT ou use sql.number() (infere automaticamente INT para valores em [-2^31, 2^31-1], recorrendo a BIGINT para valores maiores) / sql.int() no ponto da chamada.

Tipos -- @param suportados (não diferenciam maiúsculas de minúsculas):

  • STRING, BOOLEAN, DATE, TIMESTAMP, BINARY
  • INT, BIGINT, TINYINT, SMALLINT — vincule com sql.int() / sql.bigint()
  • FLOAT, DOUBLE — vincule com sql.float() / sql.double()
  • NUMERIC, DECIMAL — vincule com sql.numeric() (passe strings para manter a precisão)

Valores de exemplo para geração de tipos

Algumas queries só têm um formato válido depois que um parâmetro recebe um valor concreto — o caso mais comum é o nome de tabela dinâmico construído com IDENTIFIER(). Durante a geração de tipos, o AppKit executa DESCRIBE QUERY com valores padrão de preenchimento, de modo que um parâmetro não resolvido vira uma string vazia e gera SQL inválido (IDENTIFIER('' || '.schema.table')PARSE_SYNTAX_ERROR).

Acrescente = value a uma anotação -- @param para fornecer um valor de exemplo à geração de tipos. Ele é usado apenas ao descrever a query; em runtime, o parâmetro real continua sendo vinculado, então a query permanece portátil entre ambientes:

-- @param target_catalog STRING = main
SELECT *
FROM IDENTIFIER(:target_catalog || '.sales.nation')

A geração de tipos descreve main.sales.nation para inferir as colunas do resultado, enquanto o app implantado vincula qualquer catálogo que o chamador passar. Valores de texto, DATE e TIMESTAMP recebem aspas automaticamente (= main'main'), e um literal já entre aspas é mantido como está (= '2024-01-01'). Valores numéricos, BOOLEAN e BINARY são validados contra um formato literal estrito (= 100, = true, = X'00'); um valor que não corresponda — ou seja, qualquer coisa que pudesse injetar SQL na instrução DESCRIBE — é ignorado, e o parâmetro passa a usar o marcador baseado no tipo, de modo que um valor de exemplo nunca consegue escapar do DESCRIBE QUERY.

Parâmetros injetados pelo servidor

:workspaceId é injetado pelo servidor e não deve ser anotado:

WHERE workspace_id = :workspaceId

Endpoints HTTP

O Analytics plugin expõe os seguintes endpoints (montados em /api/analytics):

  • POST /api/analytics/query/:query_key
  • GET /api/analytics/arrow-result/:jobId
  • POST /api/analytics/metric/:key — mede uma Metric View do Unity Catalog (consulte Metric views)

Opções de formato

  • format: "JSON" (padrão) retorna linhas em JSON
  • format: "ARROW" retorna um payload Arrow com "statement_id" via SSE e, em seguida, o cliente busca o Arrow binário em /api/analytics/arrow-result/:jobId

Metric views

POST /api/analytics/metric/:key mede uma Metric View do Unity Catalog que você declarou em config/metric-views/definitions.json. Em vez de escrever SQL, o chamador envia uma requisição estruturada — quais medidas agregar, por quais dimensões agrupar e um filtro opcional — e o plugin monta e executa o SELECT MEASURE(...) ... GROUP BY ALL para você sobre a view.

A rota permanece inativa enquanto config/metric-views/definitions.json não existir: sem o arquivo de configuração, toda chave de métrica retorna 404. A declaração do arquivo (e a geração de tipos) é abordada em Tipos de metric view; esta seção documenta o endpoint de runtime que essa configuração ativa.

Corpo da requisição

POST /api/analytics/metric/:key Content-Type: application/json { "measures": ["arr", "revenue"], "dimensions": ["region", "order_date"], "timeGrain": "month", "timeDimension": "order_date", "filter": { "member": "region", "operator": "in", "values": ["EMEA", "APAC"] }, "orderBy": [{ "field": "revenue", "direction": "DESC" }], "limit": 100 }

:key é uma chave de métrica de definitions.json. Os campos do corpo:

CampoTipoObrigatórioDescrição
measuresstring[]simMedidas a agregar. No mínimo 1, no máximo 50. Cada uma se torna MEASURE(<name>) AS <name>.
dimensionsstring[]nãoDimensões pelas quais agrupar (máx. 20). Selecionadas literalmente e agrupadas via GROUP BY ALL.
filterobjectnãoÁrvore de predicados estruturada convertida em uma cláusula WHERE parametrizada (veja Filtros).
timeGrainstringnãoAgrupa uma dimensão temporal em intervalos via date_trunc('<grain>', …) — por exemplo, day, month. Requer timeDimension.
timeDimensionstringnãoA única dimensão que timeGrain agrupa em intervalos. Deve ser uma das dimensions. Obrigatória sempre que timeGrain estiver definido.
orderByarraynãoArray de chaves de ordenação {field, direction} (máx. 20). field deve ser uma medida ou dimensão selecionada. direction é "ASC" (padrão, omitido do SQL) ou "DESC". Ordene medidas pelo alias usado no SELECT.
limitnumbernãoLimite de linhas, um inteiro positivo (máx. 100000).
formatstringnãoJSON_ARRAY (padrão). JSON é aceito como alias obsoleto equivalente; formatos Arrow (ARROW, ARROW_STREAM) são rejeitados nesta rota.

Medidas e dimensões devem ser únicas entre as duas listas — um nome não pode se repetir, nem aparecer ao mesmo tempo como medida e como dimensão.

Como a requisição vira SQL

Dada uma view registrada como catalog.schema.revenue_metrics, a requisição acima gera (medidas e dimensões são ordenadas para garantir uma lista SELECT determinística):

SELECT MEASURE(`arr`) AS `arr`, MEASURE(`revenue`) AS `revenue`,
       date_trunc('month', `order_date`) AS `order_date`, `region`
FROM `catalog`.`schema`.`revenue_metrics`
WHERE `region` IN (:f_0, :f_1)
GROUP BY ALL
ORDER BY `revenue` DESC, `order_date`, `region`
LIMIT 100

O FQN da metric view e todos os identificadores de medidas/dimensões são delimitados por crases; os valores de filtro são vinculados como parâmetros (:f_0, :f_1, …), nunca interpolados na string SQL.

Resultados determinísticos com limit

Quando limit é definido, a rota acrescenta automaticamente todas as dimensões agrupadas à cláusula ORDER BY como critérios de desempate (a menos que já estejam indicadas em orderBy). Com GROUP BY ALL, a tupla completa de dimensões é única por linha, portanto ordenar por todas as dimensões produz uma ordem TOTAL — cada run retorna as mesmas linhas, e não uma amostra arbitrária.

Isso é importante porque LIMIT sem ORDER BY devolve uma amostra de linhas, e não "as n primeiras": o Spark retorna as linhas que produziu primeiro, o que varia conforme o particionamento, o paralelismo e o estado do cache. Um cartão construído sobre uma requisição desse tipo pode exibir um número diferente a cada run, sem que nada gere erro. Os critérios de desempate resolvem isso — com dados inalterados, a mesma requisição passa a retornar sempre as mesmas linhas.

Se você quiser os N primeiros por uma medida, ordene essa medida explicitamente e forneça limit:

{ "orderBy": [{ "field": "revenue", "direction": "DESC" }], "limit": 100 }

A rota adiciona as dimensões restantes (order_date, region no exemplo acima) após a sua entrada explícita, de modo que o resultado permanece estável entre runs.

Importante: ordene as medidas pelo alias do SELECT. O Spark rejeita ORDER BY MEASURE(\revenue`)comMETRIC_VIEW_INVALID_MEASURE_FUNCTION_INPUT. O SQL gerado atribui um alias a cada medida (por exemplo, MEASURE(`revenue`) AS `revenue`), portanto referencie sempre o alias — neste caso, apenas "revenue"`.

Filtros

filter é uma árvore recursiva. Uma folha é um único predicado:

{ "member": "region", "operator": "equals", "values": ["EMEA"] }

Os predicados podem ser combinados em grupos and / or, que podem ser aninhados:

{
  "and": [
    { "member": "region", "operator": "in", "values": ["EMEA", "APAC"] },
    {
      "or": [
        { "member": "segment", "operator": "equals", "values": ["Enterprise"] },
        { "member": "deal_size", "operator": "gt", "values": [50000] }
      ]
    }
  ]
}

O vocabulário de operadores:

OperadorSQLValores
equals=exatamente um
notEquals<>exatamente um
inIN (…)um ou mais
notInNOT IN (…)um ou mais
gt / gte / lt / lte> / >= / < / <=exatamente um
containsLIKE :paramexatamente uma string
notContainsNOT LIKE :paramexatamente uma string
setIS NOT NULLnenhum
notSetIS NULLnenhum

Em contains / notContains, os curingas %…% são aplicados ao valor do parâmetro vinculado (%value%), e não escritos no texto SQL — ou seja, o valor nunca é interpolado, assim como em todos os outros operadores.

Grupos vazios de qualquer tipo ({ "or": [] }, { "and": [] }) são rejeitados com 400 — todo grupo and / or deve conter pelo menos um predicado. Para não aplicar nenhum filtro, omita completamente o campo filter em vez de passar um grupo vazio.

Os filtros têm limites para impedir que entradas maliciosas esgotem os recursos do servidor: profundidade de aninhamento ≤ 8, ≤ 100 filhos por grupo and / or e ≤ 1000 valores por predicado. Requisições que excedam um desses limites são rejeitadas com 400.

Executores (escopo de cache)

Cada entrada em definitions.json indica o executor com o qual a query é executada, o que também define o escopo do cache. Isso é definido pela configuração, e não pela requisição:

executorExecuta comoCache
app_service_principal (padrão)O service principal do appCompartilhado entre todos os usuários
userO usuário solicitante (on-behalf-of)Por usuário

Isso reflete a distinção entre <key>.sql e <key>.obo.sql usada nas queries baseadas em arquivos.

Resposta

A resposta é o mesmo fluxo SSE de POST /api/analytics/query/:query_key. Se o SQL warehouse estiver frio, primeiro são emitidos eventos warehouse_status (consulte Prontidão do warehouse) e, em seguida, um único evento result com as linhas como objetos:

{
  "type": "result",
  "data": [
    {
      "region": "EMEA",
      "order_date": "2025-01-01",
      "arr": 1200000,
      "revenue": 340000
    }
  ]
}

Em caso de falha, emite um evento error.

Erros e comportamento

StatusCorpoQuando
404{ "error": "Metric not found" }:key não está declarada em definitions.json (também é a resposta para qualquer chave quando o arquivo não existe).
400{ "error": "Invalid metric request body (fields: …)", "code": … }O corpo da requisição não passa na validação. A mensagem indica apenas os caminhos dos campos problemáticos, nunca os valores enviados.
503{ "error": "Metric registry not available", "code": "METRIC_REGISTRY_LOAD_FAILED" }definitions.json existe, mas está malformado ou ilegível.

As edições em definitions.json passam a valer já na requisição seguinte — não é preciso reiniciar o servidor. Da mesma forma, um arquivo que estava malformado volta a funcionar na requisição seguinte assim que você o corrigir.

Uso no frontend

useAnalyticsQuery

Hook do React que se inscreve em uma consulta de analytics via SSE e retorna seu resultado mais recente.

import { useAnalyticsQuery } from "@databricks/appkit-ui/react";

const { data, loading, error } = useAnalyticsQuery(
  queryKey,
  parameters,
  options,
);

Tipo de retorno:

{
  data: T | null; // resultado da consulta (array tipado para JSON, TypedArrowTable para ARROW)
  loading: boolean; // true enquanto a consulta está em execução
  error: string | null; // mensagem de erro, ou null em caso de sucesso
  warehouseStatus: WarehouseStatus | null; // consulte "Prontidão do warehouse" abaixo
}

Opções:

OpçãoTipoPadrãoDescrição
format"JSON" | "ARROW""JSON"Formato da resposta
maxParametersSizenumber102400Tamanho máximo dos parâmetros serializados, em bytes
autoStartbooleantrueIniciar a consulta na montagem

Prontidão do warehouse

Se o SQL warehouse configurado estiver STOPPED ou STARTING no momento em que uma consulta for solicitada, o Analytics plugin irá:

  1. Iniciar o warehouse automaticamente (quando estiver STOPPED).
  2. Monitorar o estado do warehouse e transmitir eventos warehouse_status via SSE até que ele chegue a RUNNING.
  3. Executar a instrução SQL.

Ou seja, uma inicialização a frio não trava mais a interface em um spinner parado. Tanto useAnalyticsQuery quanto useMetricView expõem o status mais recente da requisição atual por meio de warehouseStatus; renderize-o para dar feedback aos usuários:

import { useAnalyticsQuery } from "@databricks/appkit-ui/react";

function SpendTable() {
  const { data, loading, error, warehouseStatus } = useAnalyticsQuery(
    "spend_summary",
    params,
  );

  if (warehouseStatus && warehouseStatus.state !== "RUNNING") {
    return <div>Warehouse is {warehouseStatus.state.toLowerCase()}…</div>;
  }
  if (loading) return <div>Loading…</div>;
  if (error) return <div>{error}</div>;
  return <table>{/* renderizar dados */}</table>;
}

Em ambos os hooks, warehouseStatus volta a null quando uma requisição é iniciada e permanece assim até a chegada do primeiro evento de status. Depois que o servidor observar o warehouse em RUNNING uma vez, as requisições seguintes dentro de ~30s ignoram completamente a verificação de prontidão e warehouseStatus permanece null, de modo que o hot path em regime estável não sofre nenhum round-trip adicional.

Se o warehouse estiver em DELETED/DELETING ou não atingir RUNNING dentro do tempo limite configurado, a rota emite um evento error (exposto pelo campo error).

Indicador global de prontidão

Em dashboards com muitos gráficos, um spinner por componente não basta — repetir a mesma interface de "warehouse aquecendo" em cada skeleton é trabalhoso e redundante. O AppKit traz um pequeno contexto genérico (ResourceStatusProvider) + um indicador pronto para uso (ResourceStatusIndicator) no qual qualquer plugin pode publicar; os warehouses de analytics são conectados automaticamente.

O indicador exibe o pior status pendente como um toast do sonner, herdando assim as animações, a tematização e o empilhamento do sonner. O componente monta seu próprio <Toaster /> (no canto superior direito, por padrão) e repassa suas props (position, theme, richColors, …):

import {
  ResourceStatusIndicator,
  ResourceStatusProvider,
} from "@databricks/appkit-ui/react";

export function AppShell({ children }) {
  return (
    <ResourceStatusProvider>
      <ResourceStatusIndicator />
      {children}
    </ResourceStatusProvider>
  );
}

useAnalyticsQuery e useMetricView se registram automaticamente no provider mais próximo, dispensando qualquer configuração por gráfico. O indicador renderiza apenas o ponto de montagem <Toaster /> enquanto todos os recursos estão saudáveis; quando necessário, exibe um único toast fixo — toast.loading para inicializações a frio e toast.error para estados irrecuperáveis — identificado pelo tipo mais crítico, e o descarta assim que todos se estabilizam. Como o mesmo provider é compartilhado entre os tipos de recurso (warehouse, lakebase, model serving, …), um único indicador cobre todos os plugins.

Se você já renderiza seu próprio <Toaster /> para toasts não relacionados da aplicação, remova o indicador e chame useResourceStatusToaster(), assim os toasts de status de recursos passam a usar esse mesmo Toaster:

import { useResourceStatusToaster, Toaster } from "@databricks/appkit-ui/react";

function App() {
  useResourceStatusToaster();
  return (
    <>
      <Toaster position="top-right" />
      <Routes />
    </>
  );
}

Para um corpo de toast totalmente personalizado, passe render (renderizado via toast.custom):

<ResourceStatusIndicator
  render={(agg) => (
    <div className="rounded-lg border bg-background p-3 shadow">
      {agg.worst?.kind} {agg.worst?.state.toLowerCase()} ({agg.activeCount}{" "}
      waiting)
    </div>
  )}
/>

Para substituir os textos de um tipo específico sem reescrever toda a interface, passe renderers:

<ResourceStatusIndicator
  renderers={{
    warehouse: {
      title: () => "Spinning up your data",
      description: (_s, agg) => `${agg.affectedLabels.length} chart(s) waiting`,
    },
  }}
/>

Ou crie sua própria interface a partir do agregado com useResourceStatus():

import { useResourceStatus } from "@databricks/appkit-ui/react";

// Pior status entre todos os tipos
const aggregate = useResourceStatus();
// Apenas warehouses
const warehouseOnly = useResourceStatus({ kind: "warehouse" });
// { worst, byKind, affectedLabels, activeCount, elapsedMs }

O provider é opcional. Aplicações que não o montam continuam recebendo o campo warehouseStatus de cada hook, e o hook funciona exatamente como antes.

Publicando o status dos seus próprios recursos

Plugins (ou seu próprio código) podem se integrar ao mesmo provider para recursos que não sejam de analytics — por exemplo, uma conexão Lakebase Postgres em aquecimento ou um serving endpoint de modelo em cold start:

import { useResourceStatusPublisher } from "@databricks/appkit-ui/react";
import { useEffect, useId } from "react";

function useLakebaseReadiness() {
  const id = useId();
  const { publish, unpublish } = useResourceStatusPublisher(id, "lakebase", {
    kindHint: "lakebase",
  });

  useEffect(() => {
    publish({
      kind: "lakebase",
      state: "STARTING",
      severity: "pending",
      startedAt: Date.now(),
    });
    return () => unpublish();
  }, [publish, unpublish]);
}

Configuração do servidor (em analytics({...})):

OpçãoTipoPadrãoDescrição
warehouseStartupTimeoutMsnumber300000 (5 min)Tempo máximo de espera até o warehouse atingir o estado RUNNING antes de a requisição falhar
autoStartWarehousebooleantrueQuando true, um warehouse STOPPED é iniciado automaticamente na primeira requisição. Defina como false em deployments com custo controlado, nos quais inicializações faturáveis do warehouse não devem ser disparadas por requisições de usuários; nesse caso, STOPPED é reportado como um ConfigurationError

Exemplo com tratamento de carregamento/erro/vazio:

import { useAnalyticsQuery } from "@databricks/appkit-ui/react";
import { sql } from "@databricks/appkit-ui/js";
import { Skeleton } from "@databricks/appkit-ui";

function SpendTable() {
  const params = useMemo(
    () => ({
      startDate: sql.date("2025-01-01"),
      endDate: sql.date("2025-12-31"),
    }),
    [],
  );

  const { data, loading, error } = useAnalyticsQuery("spend_summary", params);

  if (loading) return <Skeleton className="h-32 w-full" />;
  if (error) return <div className="text-destructive">{error}</div>;
  if (!data?.length)
    return <div className="text-muted-foreground">No results</div>;

  return (
    <ul>
      {data.map((row) => (
        <li key={row.id}>
          {row.name}: ${row.cost_usd}
        </li>
      ))}
    </ul>
  );
}

Queries com tipagem segura

Estenda a interface QueryRegistry para obter inferência completa de tipos em parâmetros e resultados:

// shared/appkit-types/analytics.d.ts
declare module "@databricks/appkit-ui/react" {
  interface QueryRegistry {
    spend_summary: {
      name: "spend_summary";
      parameters: { startDate: string; endDate: string };
      result: Array<{ id: string; name: string; cost_usd: number }>;
    };
  }
}

Consulte Geração de tipos para saber como gerar tipos automaticamente a partir de arquivos SQL.

Memoização

Sempre envolva os parâmetros em useMemo para evitar loops de refetch. O hook é reexecutado sempre que a referência dos parâmetros muda:

// Correto
const params = useMemo(() => ({ status: sql.string("active") }), []);
const { data } = useAnalyticsQuery("users", params);

// Errado - cria um novo objeto a cada renderização, causando refetches infinitos
const { data } = useAnalyticsQuery("users", { status: sql.string("active") });

useMetricView

Hook React que mede uma metric view via SSE — o equivalente, no cliente, de POST /api/analytics/metric/:key. Em vez de escrever SQL, você passa as medidas, as dimensões e o filtro como uma requisição estruturada; o hook devolve as linhas em streaming, com nomes de colunas tipados e metadados de exibição por coluna.

import { useMetricView } from "@databricks/appkit-ui/react";

const { data, loading, error, errorCode, metadata, warehouseStatus } =
  useMetricView("revenue", {
    measures: ["arr", "mrr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });

Quando "revenue" é uma chave do MetricRegistry gerado (veja Tipos de metric view), os nomes de medida/dimensão, os valores permitidos de timeGrain e as chaves de linha selecionadas são todos inferidos — passar uma medida desconhecida é um erro de tipo. JSON_ARRAY preserva as células escalares SQL como strings e permite SQL NULL em todas as colunas, portanto data tem o tipo Array<{ arr: string | null; mrr: string | null; created_at: string | null }> | null; use metadata[col].type quando quiser interpretar um valor intencionalmente.

Queries de série temporal devem ordenar explicitamente a dimensão de tempo selecionada de forma ascendente, como acima. Fora isso, a ordem dos resultados SQL é indefinida; os helpers de gráfico podem normalizar dados cronológicos de forma defensiva, mas não convém depender disso para a ordenação da query.

Opções:

OpçãoTipoObrigatórioDescrição
measuresstring[]simMedidas a agregar. Inferidas de MetricRegistry[key].measureKeys para uma chave conhecida.
dimensionsstring[]nãoDimensões para agrupar. Inferidas de measureKeys / dimensionKeys.
filterMetricFilternãoÁrvore recursiva de predicados (mesma gramática da rota — veja Filtros).
timeGrainstringnãoAgrupa uma dimensão de tempo em buckets (day, month, …). Requer timeDimension. Valores inferidos de timeGrains.
timeDimensionstringnãoA única dimensão que timeGrain divide em buckets. Deve ser uma das dimensions.
orderBy{field, direction?}[]nãoChaves de ordenação. field fica restrito às medidas/dimensões selecionadas nesta chamada, portanto ordenar por uma coluna não selecionada é um erro de tipo. Veja Resultados determinísticos com limit.
limitnumbernãoLimite de linhas, inteiro positivo.
autoStartbooleannãoInicia a query de métrica automaticamente. O padrão é true; defina como false para adiá-la até que a opção se torne true.

Tipo de retorno:

{
  data: T | null; // chaves das linhas selecionadas com valores JSON_ARRAY string | null
  loading: boolean; // true enquanto a consulta de métrica está em execução
  error: string | null; // mensagem sanitizada e legível por humanos, ou null em caso de sucesso
  errorCode: string | null; // código estável da origem (use-o em condicionais, não a mensagem)
  metadata: Record<string, MetricViewColumnDisplay> | undefined; // metadados de exibição por coluna (veja abaixo)
  warehouseStatus: WarehouseStatus | null; // status de prontidão mais recente da requisição atual
}

Assim como em useAnalyticsQuery, o objeto de opções é serializado (JSON.stringify) internamente, portanto literais de objeto/array passados novamente a cada renderização não disparam uma nova busca, desde que sejam serializados para a mesma string — você não precisa aplicar useMemo às opções. (Trata-se de mesma serialização, e não de igualdade estrutural profunda: reordenar as chaves dentro de filter altera a string e provoca, sim, uma nova consulta. Mover measures/dimensions para o escopo do módulo ou memoizá-los continua sendo válido e mantém os arrays com o tipo restrito à sua tupla literal.)

metadata são os metadados de exibição por coluna apenas das colunas que você consultou, delimitados e transportados no payload result do SSE. Seu valor é undefined quando o servidor não resolveu nenhum metadado (a chave da métrica é desconhecida ou os tipos não foram gerados) — portanto, sempre o trate como opcional.

Metadados

A rota de métricas adiciona metadados de exibição por coluna (display_name, format, type, description) a cada mensagem result. Esses metadados são gerados no build pelo gerador de tipos de metric view, que os grava em config/metric-views/metadata.generated.json, ao lado do seu definitions.json escrito manualmente.

Não é necessária nenhuma configuração. O plugin descobre o bundle da mesma forma que descobre o definitions.json, portanto basta analytics({}):

// server/index.ts
import { analytics, createApp, server } from "@databricks/appkit";

createApp({
  plugins: [
    server(),
    analytics({}),
    // …
  ],
});

Faça commit de metadata.generated.json junto com os tipos gerados — ele é a metade em runtime do mesmo processo de geração, e a rota o lê do disco no momento da requisição.

Isso é pura decoração da resposta: os metadados nunca entram na chave de cache e nunca alteram o SQL. Toda mensagem result de métrica carrega um campo metadata restrito às colunas solicitadas; quando não há bundle, a mensagem é idêntica byte a byte a um resultado comum de /query e o metadata do hook fica undefined. Um bundle ausente ou malformado degrada para colunas sem rótulo e registra um aviso nos logs — mas nunca faz a consulta falhar. Como os metadados viajam junto com o payload, o cliente nunca precisa importar o arquivo gerado nem embutir uma string de formato — eles são transportados pelo payload e agnósticos ao cliente.

Para ignorar o arquivo por completo — no caso de um app que monta seus metadados de outra forma, ou que os fixa deliberadamente — passe analytics({ metricViewsMetadata }). Um valor explícito sempre prevalece sobre o bundle descoberto.

Utilitários de formatação

O @databricks/appkit-ui/js traz formatadores pequenos, puros e compatíveis com tree-shaking que convertem valores brutos + os metadados acima em strings de exibição. Eles recebem a especificação de formato (ou MetricViewColumnDisplay) como argumentos — sem React, sem acoplamento a bibliotecas de gráficos — de modo que funcionam igualmente bem em tabelas, tooltips e configurações de gráficos.

FunçãoPropósito
formatValue(value, format?)Formata um valor bruto com uma especificação de formato UC/planilha ("$#,##0.00", "#,##0", "0.0%"). Sem especificação → padrão razoável.
formatLabel(name, columnMeta?)Rótulo legível para uma coluna: dá preferência a columnMeta.display_name; caso contrário, humaniza o nome bruto.
toD3Format(format?)Divide um formato UC em um specifier do d3-format e um prefix literal de moeda.

A regra de ouro: obtenha o formato do metadata, nunca o digite manualmente. Quando metadata é undefined, metadata?.[col]?.format também é undefined e formatValue recorre a um padrão sem quebrar:

import { formatLabel, formatValue } from "@databricks/appkit-ui/js";
import { useMetricView } from "@databricks/appkit-ui/react";

function RevenueTable() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr", "mrr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });
  const columns = ["created_at", "arr", "mrr"] as const;

  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            // Texto do cabeçalho vindo de display_name (ou um fallback legível).
            <th key={col}>{formatLabel(col, metadata?.[col])}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data?.map((row, i) => (
          <tr key={i}>
            {columns.map((col) => (
              // A string de formato vem de metadata, nunca escrita à mão.
              <td key={col}>{formatValue(row[col], metadata?.[col]?.format)}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Alimentando gráficos com o formato

Como metadata[col].format é apenas uma string no payload, a mesma especificação controla as marcações dos eixos e os tooltips em qualquer biblioteca de gráficos.

Gráficos do AppKit — passe um valueFormatter para o gráfico integrado. O segundo argumento é o campo da medida, portanto um único callback pode selecionar o formato do catálogo para cada série. O gráfico o aplica ao seu eixo de valores integrado e aos tooltips de cada série, sem substituir os padrões internos yAxis ou tooltip do ECharts:

import { formatValue } from "@databricks/appkit-ui/js";
import { LineChart, useMetricView } from "@databricks/appkit-ui/react";

function RevenueChart() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr", "mrr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });

  if (!data) return null;

  return (
    <LineChart
      data={data}
      xKey="created_at"
      yKey={["arr", "mrr"]}
      valueFormatter={(value, field) =>
        formatValue(value, metadata?.[field]?.format)
      }
    />
  );
}

Quando várias séries compartilham um mesmo eixo de valores, seus ticks usam o primeiro yKey; cada tooltip usa o campo correspondente da série.

A prop selected adiciona destaque declarativo apenas a gráficos de barras, de pizza e de rosca. Gráficos de linha, área, dispersão, mapa de calor e radar a ignoram, pois a semântica de seleção de categorias não está definida para esses tipos de gráfico.

Plotly — passe o especificador numérico como tickformat e o símbolo monetário literal como tickprefix. É necessário mantê-los separados porque o marcador $ do d3 depende da localidade e não consegue representar símbolos arbitrários:

import Plot from "react-plotly.js";
import { toD3Format } from "@databricks/appkit-ui/js";
import { useMetricView } from "@databricks/appkit-ui/react";

function RevenuePlot() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });
  const arrFormat = toD3Format(metadata?.arr?.format);
  // "€#,##0.00" → { specifier: ",.2f", prefix: "€" }

  return (
    <Plot
      data={[
        {
          type: "scatter",
          mode: "lines+markers",
          x: data?.map((r) => r.created_at) ?? [],
          y: data?.map((r) => r.arr) ?? [],
          name: metadata?.arr?.display_name ?? "arr",
        },
      ]}
      layout={{
        yaxis: {
          tickformat: arrFormat?.specifier,
          tickprefix: arrFormat?.prefix,
        },
        hoverlabel: { namelength: -1 },
      }}
    />
  );
}

ECharts — use a especificação de formato dentro de axisLabel.formatter / tooltip.formatter via formatValue:

import ReactECharts from "echarts-for-react";
import { formatLabel, formatValue } from "@databricks/appkit-ui/js";
import { useMetricView } from "@databricks/appkit-ui/react";

function RevenueECharts() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });
  const arrFormat = metadata?.arr?.format;

  const option = {
    xAxis: { type: "category", data: data?.map((r) => r.created_at) ?? [] },
    yAxis: {
      type: "value",
      axisLabel: { formatter: (v: number) => formatValue(v, arrFormat) },
    },
    tooltip: {
      trigger: "axis",
      valueFormatter: (v: number) => formatValue(v, arrFormat),
    },
    series: [
      {
        name: formatLabel("arr", metadata?.arr),
        type: "line",
        data: data?.map((r) => r.arr) ?? [],
      },
    ],
  };

  return <ReactECharts option={option} />;
}

Em ambos os casos, a string de formato vem do metadata injetado pelo servidor e nunca é escrita no componente — basta trocar o atributo format do YAML na metric view para reformatar todos os eixos, tooltips e células de tabela sem nenhuma alteração no cliente.

Databricks Developer Hub

Pronto para lançar seu próximo aplicativo baseado em agentes em minutos?

Ler a documentação