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
.sqlemconfig/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 :limitLIMIT / 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,BINARYINT,BIGINT,TINYINT,SMALLINT— vincule comsql.int()/sql.bigint()FLOAT,DOUBLE— vincule comsql.float()/sql.double()NUMERIC,DECIMAL— vincule comsql.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 = :workspaceIdEndpoints HTTP
O Analytics plugin expõe os seguintes endpoints (montados em /api/analytics):
POST /api/analytics/query/:query_keyGET /api/analytics/arrow-result/:jobIdPOST /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 JSONformat: "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:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
measures | string[] | sim | Medidas a agregar. No mínimo 1, no máximo 50. Cada uma se torna MEASURE(<name>) AS <name>. |
dimensions | string[] | não | Dimensões pelas quais agrupar (máx. 20). Selecionadas literalmente e agrupadas via GROUP BY ALL. |
filter | object | não | Árvore de predicados estruturada convertida em uma cláusula WHERE parametrizada (veja Filtros). |
timeGrain | string | não | Agrupa uma dimensão temporal em intervalos via date_trunc('<grain>', …) — por exemplo, day, month. Requer timeDimension. |
timeDimension | string | não | A única dimensão que timeGrain agrupa em intervalos. Deve ser uma das dimensions. Obrigatória sempre que timeGrain estiver definido. |
orderBy | array | não | Array 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. |
limit | number | não | Limite de linhas, um inteiro positivo (máx. 100000). |
format | string | não | JSON_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 100O 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:
| Operador | SQL | Valores |
|---|---|---|
equals | = | exatamente um |
notEquals | <> | exatamente um |
in | IN (…) | um ou mais |
notIn | NOT IN (…) | um ou mais |
gt / gte / lt / lte | > / >= / < / <= | exatamente um |
contains | LIKE :param | exatamente uma string |
notContains | NOT LIKE :param | exatamente uma string |
set | IS NOT NULL | nenhum |
notSet | IS NULL | nenhum |
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:
executor | Executa como | Cache |
|---|---|---|
app_service_principal (padrão) | O service principal do app | Compartilhado entre todos os usuários |
user | O 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
| Status | Corpo | Quando |
|---|---|---|
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ção | Tipo | Padrão | Descrição |
|---|---|---|---|
format | "JSON" | "ARROW" | "JSON" | Formato da resposta |
maxParametersSize | number | 102400 | Tamanho máximo dos parâmetros serializados, em bytes |
autoStart | boolean | true | Iniciar 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á:
- Iniciar o warehouse automaticamente (quando estiver
STOPPED). - Monitorar o estado do warehouse e transmitir eventos
warehouse_statusvia SSE até que ele chegue aRUNNING. - 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ção | Tipo | Padrão | Descrição |
|---|---|---|---|
warehouseStartupTimeoutMs | number | 300000 (5 min) | Tempo máximo de espera até o warehouse atingir o estado RUNNING antes de a requisição falhar |
autoStartWarehouse | boolean | true | Quando 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ção | Tipo | Obrigatório | Descrição |
|---|---|---|---|
measures | string[] | sim | Medidas a agregar. Inferidas de MetricRegistry[key].measureKeys para uma chave conhecida. |
dimensions | string[] | não | Dimensões para agrupar. Inferidas de measureKeys / dimensionKeys. |
filter | MetricFilter | não | Árvore recursiva de predicados (mesma gramática da rota — veja Filtros). |
timeGrain | string | não | Agrupa uma dimensão de tempo em buckets (day, month, …). Requer timeDimension. Valores inferidos de timeGrains. |
timeDimension | string | não | A única dimensão que timeGrain divide em buckets. Deve ser uma das dimensions. |
orderBy | {field, direction?}[] | não | Chaves 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. |
limit | number | não | Limite de linhas, inteiro positivo. |
autoStart | boolean | não | Inicia 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ção | Propó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.