Analytics plugin
Analytics plugin
Permite ejecutar queries SQL en SQL Warehouses de Databricks.
Características principales:
- Queries SQL basadas en archivos con generación automática de tipos
- Queries parametrizadas con helpers SQL con seguridad de tipos
- Compatibilidad con los formatos JSON y Arrow
- Caché y lógica de reintentos integradas
- Streaming con Server-Sent Events (SSE)
Uso básico
import { analytics, createApp, server } from "@databricks/appkit";
await createApp({
plugins: [server(), analytics({})],
});Archivos de query
- Coloca los archivos
.sqlenconfig/queries/ - La clave de la query es el nombre del archivo sin
.sql(por ejemplo,spend_summary.sql→"spend_summary")
Contexto de ejecución
queryKey.sqlse ejecuta como service principal (caché compartida)queryKey.obo.sqlse ejecuta como usuario (OBO = on-behalf-of, caché por usuario)
El contexto de ejecución lo determina el nombre del archivo SQL, no la llamada al hook.
Parámetros SQL
Usa marcadores de posición :paramName y, opcionalmente, anota los tipos de parámetros mediante comentarios SQL:
-- @param startDate DATE
-- @param endDate DATE
-- @param limit INT
SELECT ...
WHERE usage_date BETWEEN :startDate AND :endDate
LIMIT :limitLIMIT / OFFSET requieren específicamente el tipo IntegerType de Spark; BIGINT
(LongType) se rechaza con INVALID_LIMIT_LIKE_EXPRESSION.DATA_TYPE.
Anota con INT o usa sql.number() (infiere automáticamente INT para valores en
[-2^31, 2^31-1] y recurre a BIGINT para valores más amplios) / sql.int()
en el punto de llamada.
Tipos -- @param admitidos (no distinguen entre mayúsculas y minúsculas):
STRING,BOOLEAN,DATE,TIMESTAMP,BINARYINT,BIGINT,TINYINT,SMALLINT: se enlazan consql.int()/sql.bigint()FLOAT,DOUBLE: se enlazan consql.float()/sql.double()NUMERIC,DECIMAL: se enlazan consql.numeric()(pasa cadenas para conservar la precisión)
Valores de muestra para la generación de tipos
Algunas queries solo tienen una forma válida cuando un parameter tiene un valor concreto; el
caso más habitual es un nombre de tabla dinámico construido con IDENTIFIER(). Durante la generación de tipos,
AppKit ejecuta DESCRIBE QUERY con valores predeterminados de marcador de posición, por lo que un parameter sin resolver
queda reducido a una cadena vacía y genera SQL no válido
(IDENTIFIER('' || '.schema.table') → PARSE_SYNTAX_ERROR).
Añade = value a una anotación -- @param para proporcionar un valor de muestra a la generación
de tipos. Solo se usa al describir la query; en runtime se sigue vinculando el parameter
real, de modo que la query se mantiene portable entre entornos:
-- @param target_catalog STRING = main
SELECT *
FROM IDENTIFIER(:target_catalog || '.sales.nation')La generación de tipos describe main.sales.nation para inferir las columnas del resultado, mientras que
la aplicación desplegada se enlaza al catálogo que indique quien la invoque. Los valores de cadena, DATE y
TIMESTAMP se entrecomillan automáticamente (= main → 'main'), y un
literal ya entrecomillado se conserva tal cual (= '2024-01-01'). Los valores numéricos, BOOLEAN y
BINARY se validan frente a un formato literal estricto (= 100, = true,
= X'00'); todo valor que no coincida —es decir, cualquier cosa que pudiera inyectar SQL
en la sentencia describe— se ignora y el parameter recurre a su
marcador de posición basado en el tipo, de modo que un valor de muestra nunca puede salirse del
DESCRIBE QUERY.
Parámetros inyectados por el servidor
:workspaceId lo inyecta el servidor y no debe anotarse:
WHERE workspace_id = :workspaceIdEndpoints HTTP
El Analytics plugin expone estos endpoints (montados en /api/analytics):
POST /api/analytics/query/:query_keyGET /api/analytics/arrow-result/:jobIdPOST /api/analytics/metric/:key: mide una Metric View de Unity Catalog (consulta Metric views)
Opciones de formato
format: "JSON"(predeterminado) devuelve filas en JSONformat: "ARROW"devuelve un payload de Arrow con «statement_id» mediante SSE y, después, el cliente obtiene el Arrow binario desde/api/analytics/arrow-result/:jobId
Metric views
POST /api/analytics/metric/:key mide una Metric View de Unity Catalog que hayas declarado en config/metric-views/definitions.json. En lugar de escribir SQL, quien hace la llamada envía una petición estructurada —qué medidas agregar, por qué dimensiones agrupar y un filtro opcional— y el plugin construye y ejecuta por ti el SELECT MEASURE(...) ... GROUP BY ALL sobre la view.
La ruta permanece inactiva mientras no exista config/metric-views/definitions.json: sin archivo de configuración, cualquier clave de métrica devuelve 404. La declaración del archivo (y la generación de tipos) se explica en Tipos de vistas de métricas; esta sección documenta el endpoint en runtime que esa configuración activa.
Cuerpo de la solicitud
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 es una clave de métrica de definitions.json. Los campos del cuerpo:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
measures | string[] | sí | Medidas que se van a agregar. Como mínimo 1 y como máximo 50. Cada una se convierte en MEASURE(<name>) AS <name>. |
dimensions | string[] | no | Dimensiones por las que agrupar (máx. 20). Se seleccionan tal cual y se agrupan mediante GROUP BY ALL. |
filter | objeto | no | Árbol de predicados estructurado que se traduce a una cláusula WHERE parametrizada (consulta Filtros). |
timeGrain | string | no | Agrupa una dimensión temporal en intervalos mediante date_trunc('<grain>', …): por ejemplo, day o month. Requiere timeDimension. |
timeDimension | string | no | La única dimensión que timeGrain agrupa en intervalos. Debe ser una de las incluidas en dimensions. Es obligatoria siempre que se defina timeGrain. |
orderBy | arreglo | no | Arreglo de claves de ordenación {field, direction} (máx. 20). field debe ser una medida o dimensión seleccionada. direction es "ASC" (valor predeterminado, se omite en el SQL) o "DESC". Ordena las medidas por su alias del SELECT. |
limit | number | no | Número entero positivo que limita las filas (máx. 100000). |
format | string | no | JSON_ARRAY (predeterminado). Se acepta JSON como alias obsoleto de este; los formatos Arrow (ARROW, ARROW_STREAM) se rechazan en esta ruta. |
Las medidas y las dimensiones deben ser únicas en ambas listas: un nombre no puede repetirse ni aparecer a la vez como medida y como dimensión.
Cómo la solicitud se convierte en SQL
Dada una vista registrada como catalog.schema.revenue_metrics, la solicitud anterior genera lo siguiente (las medidas y dimensiones se ordenan para obtener una lista SELECT determinista):
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 100El FQN de la vista de métricas y cada identificador de medida o dimensión se citan entre acentos graves; los valores de filtro se vinculan como parámetros (:f_0, :f_1, …) y nunca se interpolan en la cadena SQL.
Resultados deterministas con limit
Cuando se define limit, la ruta añade automáticamente todas las dimensiones agrupadas a la cláusula ORDER BY como criterios de desempate (a menos que ya figuren en orderBy). Con GROUP BY ALL, la tupla completa de dimensiones es única en cada fila, de modo que ordenar por todas las dimensiones produce un orden TOTAL: cada run devuelve las mismas filas, no una muestra arbitraria.
Esto es importante porque LIMIT sin ORDER BY devuelve una muestra de filas, no "las n primeras": Spark devuelve las filas que haya producido primero, lo cual varía según el particionado, el paralelismo y el estado de la caché. Una tarjeta basada en una solicitud así puede mostrar un número distinto en cada run sin que nada falle. Los criterios de desempate cierran esa brecha: si los datos no cambian, la misma solicitud devuelve siempre las mismas filas.
Si quieres los N primeros según una medida, ordena por esa medida explícitamente e indica limit:
{ "orderBy": [{ "field": "revenue", "direction": "DESC" }], "limit": 100 }La ruta añade las dimensiones restantes (order_date, region en el ejemplo anterior) después de tu entrada explícita, de modo que el resultado se mantiene estable entre runs.
Importante: ordena las medidas por su alias del SELECT. Spark rechaza ORDER BY MEASURE(\revenue`)conMETRIC_VIEW_INVALID_MEASURE_FUNCTION_INPUT. El SQL generado asigna un alias a cada medida (p. ej. MEASURE(`revenue`) AS `revenue`), así que referencia siempre el alias — en este caso, simplemente "revenue"`.
Filtros
filter es un árbol recursivo. Una hoja es un único predicado:
{ "member": "region", "operator": "equals", "values": ["EMEA"] }Los predicados se combinan con grupos and / or, que pueden anidarse:
{
"and": [
{ "member": "region", "operator": "in", "values": ["EMEA", "APAC"] },
{
"or": [
{ "member": "segment", "operator": "equals", "values": ["Enterprise"] },
{ "member": "deal_size", "operator": "gt", "values": [50000] }
]
}
]
}El vocabulario de operadores:
| Operador | SQL | Valores |
|---|---|---|
equals | = | exactamente uno |
notEquals | <> | exactamente uno |
in | IN (…) | uno o más |
notIn | NOT IN (…) | uno o más |
gt / gte / lt / lte | > / >= / < / <= | exactamente uno |
contains | LIKE :param | exactamente una cadena |
notContains | NOT LIKE :param | exactamente una cadena |
set | IS NOT NULL | ninguno |
notSet | IS NULL | ninguno |
En contains / notContains, los comodines %…% se aplican al valor del parámetro vinculado (%value%); no se escriben en el texto SQL, por lo que el valor nunca se interpola, al igual que con el resto de operadores.
Los grupos vacíos de cualquier tipo ({ "or": [] }, { "and": [] }) se rechazan con 400: todo grupo and / or debe contener al menos un predicado. Si no quieres enviar ningún filtro, omite por completo el campo filter en lugar de pasar un grupo vacío.
Los filtros están acotados para impedir que una entrada malintencionada agote el servidor: profundidad de anidamiento ≤ 8, ≤ 100 hijos por grupo and / or y ≤ 1000 valores por predicado. Toda solicitud que supere un límite se rechaza con 400.
Ejecutores (ámbito de caché)
Cada entrada de definitions.json indica el ejecutor con el que se ejecuta la query, lo que también determina el ámbito de la caché. Esto lo fija la configuración, no la solicitud:
executor | Se ejecuta como | Caché |
|---|---|---|
app_service_principal (predeterminado) | El service principal de la app | Compartida entre todos los usuarios |
user | El usuario solicitante (on-behalf-of) | Por usuario |
Esto refleja la distinción entre <key>.sql y <key>.obo.sql en las queries basadas en archivos.
Respuesta
La respuesta es el mismo flujo SSE que el de POST /api/analytics/query/:query_key. Si el SQL warehouse está frío, primero emite eventos warehouse_status (consulta Disponibilidad del warehouse) y, después, un único evento result con las filas como objetos:
{
"type": "result",
"data": [
{
"region": "EMEA",
"order_date": "2025-01-01",
"arr": 1200000,
"revenue": 340000
}
]
}Si falla, emite un evento error en su lugar.
Errores y comportamiento
| Estado | Cuerpo | Cuándo |
|---|---|---|
404 | { "error": "Metric not found" } | :key no está declarada en definitions.json (también es la respuesta para cualquier clave cuando el archivo no existe). |
400 | { "error": "Invalid metric request body (fields: …)", "code": … } | El cuerpo de la solicitud no pasa la validación. El mensaje solo indica las rutas de los campos problemáticos, nunca los valores enviados. |
503 | { "error": "Metric registry not available", "code": "METRIC_REGISTRY_LOAD_FAILED" } | definitions.json existe, pero está mal formado o no se puede leer. |
Los cambios en definitions.json surten efecto en la siguiente solicitud: no hace falta reiniciar el servidor. Del mismo modo, un archivo mal formado que corrijas empieza a funcionar en la siguiente solicitud.
Uso en el frontend
useAnalyticsQuery
Hook de React que se suscribe a una query de analítica mediante SSE y devuelve su resultado más reciente.
import { useAnalyticsQuery } from "@databricks/appkit-ui/react";
const { data, loading, error } = useAnalyticsQuery(
queryKey,
parameters,
options,
);Tipo de retorno:
{
data: T | null; // resultado de la query (array tipado para JSON, TypedArrowTable para ARROW)
loading: boolean; // true mientras la query se está ejecutando
error: string | null; // mensaje de error, o null si se ejecutó correctamente
warehouseStatus: WarehouseStatus | null; // consulta "Disponibilidad del warehouse" más abajo
}Opciones:
| Opción | Tipo | Valor por defecto | Descripción |
|---|---|---|---|
format | "JSON" | "ARROW" | "JSON" | Formato de la respuesta |
maxParametersSize | number | 102400 | Tamaño máximo en bytes de los parámetros serializados |
autoStart | boolean | true | Ejecutar la query al montar el componente |
Disponibilidad del warehouse
Si el SQL warehouse configurado está en STOPPED o STARTING cuando se solicita una query, el Analytics plugin hará lo siguiente:
- Iniciar automáticamente el warehouse (cuando esté en
STOPPED). - Sondear el estado del warehouse y transmitir eventos
warehouse_statuspor SSE hasta que pase aRUNNING. - Ejecutar la sentencia SQL.
Esto significa que un arranque en frío ya no deja la UI bloqueada con un spinner que no avanza. Tanto useAnalyticsQuery como useMetricView exponen el estado más reciente de su solicitud actual a través de warehouseStatus; muéstralo para dar retroalimentación a los usuarios:
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 datos */}</table>;
}En ambos hooks, warehouseStatus se restablece a null cuando comienza una solicitud y permanece así hasta que llega el primer evento de estado. Una vez que el servidor ha detectado el warehouse en RUNNING, las solicitudes posteriores dentro de un margen de ~30 s omiten por completo la comprobación de disponibilidad y warehouseStatus se mantiene en null, de modo que la ruta crítica en estado estable no se penaliza con viajes de ida y vuelta adicionales.
Si el warehouse está en DELETED/DELETING o no alcanza el estado RUNNING dentro del tiempo de espera configurado, la ruta emite un evento error (expuesto mediante el campo error).
Indicador global de disponibilidad
En dashboards con muchos gráficos, un spinner por componente no basta: replicar la misma UI de «el warehouse se está iniciando» en cada esqueleto resulta repetitivo. AppKit incluye un pequeño contexto genérico (ResourceStatusProvider) y un indicador listo para usar (ResourceStatusIndicator) en el que cualquier plugin puede publicar; los warehouses de analítica se conectan automáticamente.
El indicador muestra el peor estado pendiente como una notificación de sonner, por lo que hereda sus animaciones, temas y apilamiento. El componente monta su propio <Toaster /> (arriba a la derecha de forma predeterminada) y reenvía sus props (position, theme, richColors, …):
import {
ResourceStatusIndicator,
ResourceStatusProvider,
} from "@databricks/appkit-ui/react";
export function AppShell({ children }) {
return (
<ResourceStatusProvider>
<ResourceStatusIndicator />
{children}
</ResourceStatusProvider>
);
}useAnalyticsQuery y useMetricView se registran por sí solos en el proveedor más cercano, por lo que no hace falta configurar nada en cada gráfico. Mientras todos los recursos estén en buen estado, el indicador solo renderiza el punto de montaje <Toaster />; si no lo están, muestra un único toast fijo —toast.loading para arranques en frío, toast.error para estados irrecuperables— según el tipo más grave, y lo descarta cuando todos se estabilizan. Como el mismo proveedor se comparte entre los distintos tipos de recursos (warehouse, lakebase, model serving, …), un solo indicador cubre todos los plugins.
Si ya renderizas tu propio <Toaster /> para otros toasts de la aplicación, prescinde del indicador y llama en su lugar a useResourceStatusToaster(), de modo que los toasts de estado de recursos compartan ese único Toaster:
import { useResourceStatusToaster, Toaster } from "@databricks/appkit-ui/react";
function App() {
useResourceStatusToaster();
return (
<>
<Toaster position="top-right" />
<Routes />
</>
);
}Para un cuerpo de toast totalmente personalizado, pasa render (se renderiza a través de 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 sobrescribir los textos de un tipo específico sin reescribir toda la UI, pasa renderers:
<ResourceStatusIndicator
renderers={{
warehouse: {
title: () => "Spinning up your data",
description: (_s, agg) => `${agg.affectedLabels.length} chart(s) waiting`,
},
}}
/>O crea tu propia UI a partir del agregado con useResourceStatus():
import { useResourceStatus } from "@databricks/appkit-ui/react";
// El peor estado de todos los tipos
const aggregate = useResourceStatus();
// Solo warehouses
const warehouseOnly = useResourceStatus({ kind: "warehouse" });
// { worst, byKind, affectedLabels, activeCount, elapsedMs }El proveedor es opcional. Las aplicaciones que no lo monten siguen obteniendo el campo warehouseStatus de cada hook, y el hook funciona exactamente igual que antes.
Publicar el estado de tus propios recursos
Los plugins (o tu propio código) pueden conectarse al mismo proveedor para recursos ajenos a la analítica; por ejemplo, una conexión de Lakebase Postgres que se está calentando o un endpoint de serving de modelos que está arrancando en frío:
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]);
}Configuración del servidor (en analytics({...})):
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
warehouseStartupTimeoutMs | number | 300000 (5 min) | Tiempo máximo de espera para que el warehouse pase al estado RUNNING antes de que la solicitud falle |
autoStartWarehouse | boolean | true | Cuando es true, un warehouse en estado STOPPED se inicia automáticamente en la primera solicitud. Establécelo en false en deployments con control de costos, donde los inicios facturables del warehouse no deben desencadenarse por solicitudes de los usuarios; en ese caso, STOPPED se refleja como un ConfigurationError |
Ejemplo con manejo de carga, errores y estado vacío:
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 con seguridad de tipos
Amplía la interfaz QueryRegistry para obtener inferencia de tipos completa en parámetros y 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 }>;
};
}
}Consulta Generación de tipos para generarlos automáticamente a partir de archivos SQL.
Memoización
Envuelve siempre los parámetros en useMemo para evitar bucles de refetch. El hook se vuelve a ejecutar cada vez que cambia la referencia de los parámetros:
// Correcto
const params = useMemo(() => ({ status: sql.string("active") }), []);
const { data } = useAnalyticsQuery("users", params);
// Incorrecto: crea un objeto nuevo en cada render y provoca recargas infinitas
const { data } = useAnalyticsQuery("users", { status: sql.string("active") });useMetricView
Hook de React que mide una metric view mediante SSE: el equivalente en el cliente de POST /api/analytics/metric/:key. En lugar de escribir SQL, pasas las medidas, las dimensiones y el filtro como una petición estructurada; el hook devuelve en streaming las filas con nombres de columna tipados, además de metadatos de visualización por columna.
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" }],
});Cuando "revenue" es una clave del MetricRegistry generado (consulta Tipos de vistas de métricas), se infieren tanto los nombres de medidas/dimensiones como los valores permitidos de timeGrain y las claves de fila seleccionadas: pasar una medida desconocida es un error de tipos. JSON_ARRAY conserva las celdas escalares de SQL como cadenas y permite SQL NULL en todas las columnas, por lo que data se tipa como Array<{ arr: string | null; mrr: string | null; created_at: string | null }> | null; usa metadata[col].type cuando vayas a analizar un valor de forma intencionada.
Las queries de series temporales deben ordenar explícitamente en orden ascendente la dimensión temporal seleccionada, como se muestra arriba. De lo contrario, el orden de los resultados SQL queda sin especificar; los helpers de gráficos pueden normalizar los datos cronológicos de forma defensiva, pero los consumidores no deberían depender de ello para el orden de las queries.
Opciones:
| Opción | Tipo | Obligatorio | Descripción |
|---|---|---|---|
measures | string[] | sí | Medidas que se agregan. Se infieren de MetricRegistry[key].measureKeys para una clave conocida. |
dimensions | string[] | no | Dimensiones por las que agrupar. Se infieren de measureKeys / dimensionKeys. |
filter | MetricFilter | no | Árbol de predicados recursivo (misma gramática que la ruta; consulta Filtros). |
timeGrain | string | no | Agrupa una dimensión temporal en buckets (day, month, …). Requiere timeDimension. timeGrains inferido. |
timeDimension | string | no | La única dimensión que timeGrain agrupa en buckets. Debe ser una de dimensions. |
orderBy | {field, direction?}[] | no | Claves de ordenación. field se restringe a las medidas/dimensiones seleccionadas en esta llamada, por lo que ordenar por una columna no seleccionada es un error de tipos. Consulta Resultados deterministas con limit. |
limit | number | no | Límite de filas, entero positivo. |
autoStart | boolean | no | Inicia la query de métricas automáticamente. Su valor predeterminado es true; ponlo en false para aplazarla hasta que la opción pase a true. |
Tipo de retorno:
{
data: T | null; // claves de fila seleccionadas con valores JSON_ARRAY string | null
loading: boolean; // true mientras se ejecuta la consulta de métrica
error: string | null; // mensaje saneado y legible para humanos, o null si tuvo éxito
errorCode: string | null; // código estable del origen (ramifica según este, no según el mensaje)
metadata: Record<string, MetricViewColumnDisplay> | undefined; // metadatos de visualización por columna (ver más abajo)
warehouseStatus: WarehouseStatus | null; // último estado de disponibilidad para la solicitud actual
}Al igual que en useAnalyticsQuery, el objeto de opciones se serializa (JSON.stringify) internamente, por lo que los literales de objeto o array que se pasan nuevos en cada render no provocan una nueva consulta mientras se serialicen a la misma cadena: no necesitas aplicar useMemo a las opciones. (Se trata de una serialización idéntica, no de una igualdad estructural profunda: reordenar las claves dentro de filter cambia la cadena y sí vuelve a ejecutar la query. Elevar measures/dimensions al ámbito del módulo o memoizarlos sigue siendo válido y mantiene los arrays acotados a su tipo de tupla literal).
metadata son los metadatos de presentación por columna correspondientes únicamente a las columnas que consultaste, acotados y transportados en la carga útil result del SSE. Es undefined cuando el servidor no resolvió ningún metadato (la clave de métrica es desconocida o no se han generado los tipos), así que trátalo siempre como opcional.
Metadatos
La ruta de métricas añade metadatos de visualización por columna (display_name, format, type, description) a cada mensaje result. Estos metadatos se generan durante la compilación mediante el generador de tipos de metric view, que los escribe en config/metric-views/metadata.generated.json, junto al definitions.json que tú mismo creaste.
No requiere configuración. El plugin detecta el paquete de la misma forma que detecta definitions.json, así que basta con analytics({}):
// server/index.ts
import { analytics, createApp, server } from "@databricks/appkit";
createApp({
plugins: [
server(),
analytics({}),
// …
],
});Incluye metadata.generated.json en el control de versiones junto con tus tipos generados: es la mitad de runtime del mismo proceso de generación, y la ruta lo lee del disco en el momento de la solicitud.
Esto es pura decoración de la respuesta: los metadatos nunca entran en la clave de caché ni modifican el SQL. Cada mensaje result de una métrica incluye un campo metadata acotado a las columnas solicitadas; cuando no hay ningún paquete presente, el mensaje es idéntico byte a byte al resultado de un /query simple y el metadata del hook es undefined. Un paquete ausente o mal formado degrada a columnas sin etiquetar y registra una advertencia; nunca hace fallar la query. Como los metadatos viajan en el payload, el cliente nunca tiene que importar el archivo generado ni codificar de forma fija una cadena de formato: van transportados en el payload y son agnósticos del cliente.
Para omitir el archivo por completo —por ejemplo, en una aplicación que construya sus metadatos de otra forma o que los fije deliberadamente— pasa analytics({ metricViewsMetadata }). Un valor explícito siempre prevalece sobre el paquete descubierto.
Utilidades de formato
@databricks/appkit-ui/js incluye formateadores pequeños, puros y compatibles con tree-shaking que convierten los valores sin procesar + los metadatos anteriores en cadenas de visualización. Reciben la especificación de formato (o MetricViewColumnDisplay) como argumentos —sin React, sin acoplamiento a librerías de gráficos—, por lo que funcionan igual en tablas, tooltips y configuraciones de gráficos.
| Función | Propósito |
|---|---|
formatValue(value, format?) | Formatea un valor sin procesar con una especificación de formato de UC/hoja de cálculo ("$#,##0.00", "#,##0", "0.0%"). Sin especificación → valor predeterminado razonable. |
formatLabel(name, columnMeta?) | Etiqueta legible para una columna: da preferencia a columnMeta.display_name y, si no, humaniza el nombre sin procesar. |
toD3Format(format?) | Divide un formato de UC en un specifier de d3-format y un prefix literal de moneda. |
La regla de oro: obtén el formato desde metadata, nunca lo escribas a mano. Cuando metadata es undefined, metadata?.[col]?.format es undefined y formatValue recurre sin problemas a un valor predeterminado:
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 del encabezado tomado de display_name (o un valor legible por defecto).
<th key={col}>{formatLabel(col, metadata?.[col])}</th>
))}
</tr>
</thead>
<tbody>
{data?.map((row, i) => (
<tr key={i}>
{columns.map((col) => (
// La cadena de formato viene de metadata, nunca se escribe a mano.
<td key={col}>{formatValue(row[col], metadata?.[col]?.format)}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Aplicar el formato a los gráficos
Como metadata[col].format es simplemente una cadena del payload, la misma especificación gobierna las marcas de los ejes y los tooltips en cualquier biblioteca de gráficos.
Gráficos de AppKit: pasa un valueFormatter al gráfico integrado. El segundo argumento es el campo de medida, de modo que un único callback puede seleccionar el formato del catálogo para cada serie. El gráfico lo aplica a su eje de valores integrado y a los tooltips de cada serie sin reemplazar los valores predeterminados internos de yAxis ni tooltip de 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)
}
/>
);
}Cuando varias series comparten un mismo eje de valores, sus marcas de escala usan el primer yKey; cada tooltip usa el campo de serie correspondiente.
La prop selected añade énfasis declarativo únicamente a los gráficos de barras, circulares y de anillo. Los gráficos de líneas, áreas, dispersión, mapa de calor y radar la ignoran, ya que la semántica de selección de categorías no está definida para esos tipos de gráfico.
Plotly: pasa el especificador numérico como tickformat y el símbolo de moneda literal como tickprefix. Es necesario mantenerlos separados porque el marcador $ de d3 depende de la configuración regional y no puede representar símbolos arbitrarios:
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 — usa la especificación de formato dentro de axisLabel.formatter / tooltip.formatter mediante 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} />;
}En ambos casos, la cadena de formato proviene de los metadata inyectados por el servidor y nunca se escribe dentro del componente: basta con cambiar el atributo format del YAML en la vista de métricas para que se reformateen todos los ejes, los tooltips y las celdas de la tabla sin tocar el cliente.