Ir al contenido principal

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 .sql en config/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.sql se ejecuta como service principal (caché compartida)
  • queryKey.obo.sql se 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 :limit

LIMIT / 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, BINARY
  • INT, BIGINT, TINYINT, SMALLINT: se enlazan con sql.int() / sql.bigint()
  • FLOAT, DOUBLE: se enlazan con sql.float() / sql.double()
  • NUMERIC, DECIMAL: se enlazan con sql.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 = :workspaceId

Endpoints HTTP

El Analytics plugin expone estos endpoints (montados en /api/analytics):

  • POST /api/analytics/query/:query_key
  • GET /api/analytics/arrow-result/:jobId
  • POST /api/analytics/metric/:key: mide una Metric View de Unity Catalog (consulta Metric views)

Opciones de formato

  • format: "JSON" (predeterminado) devuelve filas en JSON
  • format: "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:

CampoTipoObligatorioDescripción
measuresstring[]Medidas que se van a agregar. Como mínimo 1 y como máximo 50. Cada una se convierte en MEASURE(<name>) AS <name>.
dimensionsstring[]noDimensiones por las que agrupar (máx. 20). Se seleccionan tal cual y se agrupan mediante GROUP BY ALL.
filterobjetonoÁrbol de predicados estructurado que se traduce a una cláusula WHERE parametrizada (consulta Filtros).
timeGrainstringnoAgrupa una dimensión temporal en intervalos mediante date_trunc('<grain>', …): por ejemplo, day o month. Requiere timeDimension.
timeDimensionstringnoLa única dimensión que timeGrain agrupa en intervalos. Debe ser una de las incluidas en dimensions. Es obligatoria siempre que se defina timeGrain.
orderByarreglonoArreglo 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.
limitnumbernoNúmero entero positivo que limita las filas (máx. 100000).
formatstringnoJSON_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 100

El 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:

OperadorSQLValores
equals=exactamente uno
notEquals<>exactamente uno
inIN (…)uno o más
notInNOT IN (…)uno o más
gt / gte / lt / lte> / >= / < / <=exactamente uno
containsLIKE :paramexactamente una cadena
notContainsNOT LIKE :paramexactamente una cadena
setIS NOT NULLninguno
notSetIS NULLninguno

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:

executorSe ejecuta comoCaché
app_service_principal (predeterminado)El service principal de la appCompartida entre todos los usuarios
userEl 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

EstadoCuerpoCuá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ónTipoValor por defectoDescripción
format"JSON" | "ARROW""JSON"Formato de la respuesta
maxParametersSizenumber102400Tamaño máximo en bytes de los parámetros serializados
autoStartbooleantrueEjecutar 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:

  1. Iniciar automáticamente el warehouse (cuando esté en STOPPED).
  2. Sondear el estado del warehouse y transmitir eventos warehouse_status por SSE hasta que pase a RUNNING.
  3. 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ónTipoPredeterminadoDescripción
warehouseStartupTimeoutMsnumber300000 (5 min)Tiempo máximo de espera para que el warehouse pase al estado RUNNING antes de que la solicitud falle
autoStartWarehousebooleantrueCuando 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ónTipoObligatorioDescripción
measuresstring[]Medidas que se agregan. Se infieren de MetricRegistry[key].measureKeys para una clave conocida.
dimensionsstring[]noDimensiones por las que agrupar. Se infieren de measureKeys / dimensionKeys.
filterMetricFilternoÁrbol de predicados recursivo (misma gramática que la ruta; consulta Filtros).
timeGrainstringnoAgrupa una dimensión temporal en buckets (day, month, …). Requiere timeDimension. timeGrains inferido.
timeDimensionstringnoLa única dimensión que timeGrain agrupa en buckets. Debe ser una de dimensions.
orderBy{field, direction?}[]noClaves 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.
limitnumbernoLímite de filas, entero positivo.
autoStartbooleannoInicia 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ónPropó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.

Databricks Developer Hub

¿Todo listo para lanzar tu próxima aplicación basada en agentes en minutos?

Leer la documentación