Accéder au contenu principal

Plugin Analytics

Plugin Analytics

Permet d'exécuter des requêtes SQL sur les SQL Warehouses Databricks.

Fonctionnalités clés :

  • Requêtes SQL définies dans des fichiers, avec génération automatique des types
  • Requêtes paramétrées avec des helpers SQL typés
  • Prise en charge des formats JSON et Arrow
  • Mise en cache et logique de nouvelle tentative intégrées
  • Streaming via Server-Sent Events (SSE)

Utilisation de base

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

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

Fichiers de requêtes

  • Placez les fichiers .sql dans config/queries/
  • La clé de la requête correspond au nom du fichier sans .sql (par exemple spend_summary.sql"spend_summary")

Contexte d'exécution

  • queryKey.sql s'exécute en tant que service principal (cache partagé)
  • queryKey.obo.sql s'exécute en tant qu'utilisateur (OBO = on-behalf-of, cache par utilisateur)

C'est le nom du fichier SQL qui détermine le contexte d'exécution, et non l'appel du hook.

Paramètres SQL

Utilisez des espaces réservés :paramName et, éventuellement, annotez les types de paramètres à l'aide de commentaires SQL :

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

LIMIT / OFFSET exigent spécifiquement le type Spark IntegerTypeBIGINT (LongType) est rejeté avec INVALID_LIMIT_LIKE_EXPRESSION.DATA_TYPE. Annotez avec INT, ou utilisez sql.number() (qui déduit automatiquement INT pour les valeurs comprises dans [-2^31, 2^31-1], avec repli sur BIGINT pour les valeurs plus grandes) / sql.int() à l'endroit de l'appel.

Types -- @param pris en charge (insensibles à la casse) :

  • STRING, BOOLEAN, DATE, TIMESTAMP, BINARY
  • INT, BIGINT, TINYINT, SMALLINT — liaison via sql.int() / sql.bigint()
  • FLOAT, DOUBLE — liaison via sql.float() / sql.double()
  • NUMERIC, DECIMAL — liaison via sql.numeric() (passez des chaînes pour la précision)

Valeurs d'exemple pour la génération de types

Certaines requêtes n'ont une forme valide qu'une fois qu'un paramètre possède une valeur concrète — le plus souvent un nom de table dynamique construit avec IDENTIFIER(). Pendant la génération de types, AppKit exécute DESCRIBE QUERY avec des valeurs par défaut fictives : un paramètre non résolu se réduit donc à une chaîne vide et produit du SQL invalide (IDENTIFIER('' || '.schema.table')PARSE_SYNTAX_ERROR).

Ajoutez = value à une annotation -- @param pour fournir une valeur d'exemple à la génération de types. Elle n'est utilisée que lors de la description de la requête ; au runtime, c'est bien le paramètre réel qui est lié, si bien que la requête reste portable d'un environnement à l'autre :

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

La génération de types décrit main.sales.nation pour déduire les colonnes du résultat, tandis que l'application déployée se lie au catalogue transmis par l'appelant, quel qu'il soit. Les valeurs de type chaîne, DATE et TIMESTAMP sont automatiquement mises entre guillemets (= main'main'), et un littéral déjà entre guillemets est conservé tel quel (= '2024-01-01'). Les valeurs numériques, BOOLEAN et BINARY sont validées selon un format littéral strict (= 100, = true, = X'00') ; une valeur non conforme — c'est-à-dire tout ce qui pourrait injecter du SQL dans l'instruction de description — est ignorée et le paramètre retombe sur son espace réservé déduit du type : une valeur d'exemple ne peut donc jamais s'échapper du DESCRIBE QUERY.

Paramètres injectés par le serveur

:workspaceId est injecté par le serveur et ne doit pas être annoté :

WHERE workspace_id = :workspaceId

Endpoints HTTP

Le plugin Analytics expose les endpoints suivants (montés sous /api/analytics) :

  • POST /api/analytics/query/:query_key
  • GET /api/analytics/arrow-result/:jobId
  • POST /api/analytics/metric/:key — évalue une vue de métriques Unity Catalog (voir Metric views)

Options de format

  • format: "JSON" (valeur par défaut) renvoie des lignes JSON
  • format: "ARROW" renvoie une charge utile Arrow « statement_id » via SSE, puis le client récupère les données Arrow binaires depuis /api/analytics/arrow-result/:jobId

Vues de métriques

POST /api/analytics/metric/:key mesure une vue de métriques Unity Catalog que vous avez déclarée dans config/metric-views/definitions.json. Plutôt que d'écrire du SQL, l'appelant envoie une requête structurée — quelles mesures agréger, selon quelles dimensions regrouper et, éventuellement, un filtre — et le plugin construit puis exécute pour vous le SELECT MEASURE(...) ... GROUP BY ALL sur la vue.

La route reste inactive tant que config/metric-views/definitions.json n'existe pas : sans fichier de configuration, chaque clé de métrique renvoie 404. La déclaration du fichier (et la génération des types) est traitée dans Types de vues de métriques ; cette section décrit l'endpoint de runtime que cette configuration active.

Corps de la requête

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 est une clé de métrique issue de definitions.json. Les champs du corps :

ChampTypeRequisDescription
measuresstring[]ouiMesures à agréger. Au moins 1, au maximum 50. Chacune devient MEASURE(<name>) AS <name>.
dimensionsstring[]nonDimensions de regroupement (20 au maximum). Sélectionnées telles quelles et regroupées via GROUP BY ALL.
filterobjetnonArbre de prédicats structuré traduit en clause WHERE paramétrée (voir Filtres).
timeGrainstringnonRegroupe une dimension temporelle via date_trunc('<grain>', …) — par exemple day, month. Nécessite timeDimension.
timeDimensionstringnonL'unique dimension regroupée par timeGrain. Doit figurer dans dimensions. Obligatoire dès que timeGrain est défini.
orderBytableaunonTableau de clés de tri {field, direction} (20 au maximum). field doit être une mesure ou une dimension sélectionnée. direction vaut "ASC" (valeur par défaut, omise du SQL) ou "DESC". Triez les mesures par leur alias SELECT.
limitnumbernonNombre entier positif limitant les lignes (100000 au maximum).
formatstringnonJSON_ARRAY (valeur par défaut). JSON est accepté comme alias déprécié ; les formats Arrow (ARROW, ARROW_STREAM) sont rejetés sur cette route.

Les mesures et les dimensions doivent être uniques dans les deux listes : un nom ne peut pas être répété, ni apparaître à la fois comme mesure et comme dimension.

Comment la requête est traduite en SQL

Pour une vue enregistrée sous le nom catalog.schema.revenue_metrics, la requête ci-dessus produit (les mesures et les dimensions sont triées afin d'obtenir une liste SELECT déterministe) :

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

Le FQN de la vue de métriques ainsi que chaque identifiant de mesure ou de dimension sont encadrés par des accents graves ; les valeurs de filtre sont transmises en tant que paramètres (:f_0, :f_1, …) et ne sont jamais interpolées dans la chaîne SQL.

Résultats déterministes avec limit

Lorsque limit est défini, la route ajoute automatiquement toutes les dimensions groupées à la clause ORDER BY comme critères de départage (sauf si elles figurent déjà dans orderBy). Avec GROUP BY ALL, le tuple de dimensions complet est unique pour chaque ligne : trier selon toutes les dimensions produit donc un ordre TOTAL — chaque run renvoie les mêmes lignes, et non un échantillon arbitraire.

C'est important, car un LIMIT sans ORDER BY fournit un échantillon de lignes, et non les « n premières » : Spark renvoie les lignes qu'il a produites en premier, ce qui varie selon le partitionnement, le parallélisme et l'état du cache. Une carte construite sur une telle requête peut donc afficher un nombre différent d'un run à l'autre, sans qu'aucune erreur ne soit signalée. Les critères de départage comblent cette lacune : à données inchangées, la même requête renvoie désormais les mêmes lignes.

Si vous souhaitez obtenir le top N selon une mesure, triez explicitement sur cette mesure et fournissez limit :

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

La route ajoute les dimensions restantes (order_date, region dans l'exemple ci-dessus) après votre entrée explicite, ce qui garantit un résultat stable d'un run à l'autre.

Important : triez les mesures par leur alias SELECT. Spark rejette ORDER BY MEASURE(\revenue`)avecMETRIC_VIEW_INVALID_MEASURE_FUNCTION_INPUT. Le SQL généré attribue un alias à chaque mesure (par exemple MEASURE(`revenue`) AS `revenue`) : référencez donc toujours l'alias — ici, simplement "revenue"`.

Filtres

filter est un arbre récursif. Une feuille correspond à un prédicat unique :

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

Les prédicats se combinent au sein de groupes and / or, qui peuvent être imbriqués :

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

Le vocabulaire des opérateurs :

OpérateurSQLValeurs
equals=exactement une
notEquals<>exactement une
inIN (…)une ou plusieurs
notInNOT IN (…)une ou plusieurs
gt / gte / lt / lte> / >= / < / <=exactement une
containsLIKE :paramexactement une chaîne
notContainsNOT LIKE :paramexactement une chaîne
setIS NOT NULLaucune
notSetIS NULLaucune

Pour contains / notContains, les caractères génériques %…% s'appliquent à la valeur du paramètre lié (%value%) et ne sont pas écrits dans le texte SQL : la valeur n'est donc jamais interpolée, comme pour tous les autres opérateurs.

Les groupes vides, quel que soit leur type ({ "or": [] }, { "and": [] }), sont rejetés avec un 400 : chaque groupe and / or doit contenir au moins un prédicat. Pour n'appliquer aucun filtre, omettez complètement le champ filter au lieu de passer un groupe vide.

Les filtres sont bornés afin d'éviter qu'une entrée malveillante n'épuise les ressources du serveur : profondeur d'imbrication ≤ 8, ≤ 100 enfants par groupe and / or et ≤ 1000 valeurs par prédicat. Toute requête qui dépasse une de ces limites est rejetée avec un 400.

Exécuteurs (portée du cache)

Chaque entrée de definitions.json indique l'exécuteur sous lequel la requête s'exécute, ce qui détermine également la portée du cache. Ce choix est fixé par la configuration, et non par la requête :

executorS'exécute en tant queCache
app_service_principal (par défaut)Le service principal de l'applicationPartagé entre tous les utilisateurs
userL'utilisateur appelant (on-behalf-of)Par utilisateur

Cela correspond à la distinction entre <key>.sql et <key>.obo.sql pour les requêtes basées sur des fichiers.

Réponse

La réponse est le même flux SSE que pour POST /api/analytics/query/:query_key. Si le SQL warehouse est à froid, il émet d'abord des événements warehouse_status (voir Disponibilité du warehouse), puis un unique événement result contenant les lignes sous forme d'objets :

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

En cas d'échec, il émet plutôt un événement error.

Erreurs et comportement

StatutCorpsCas
404{ "error": "Metric not found" }:key n'est pas déclarée dans definitions.json (c'est aussi la réponse renvoyée pour toutes les clés lorsque le fichier est absent).
400{ "error": "Invalid metric request body (fields: …)", "code": … }Le corps de la requête ne passe pas la validation. Le message indique uniquement les chemins des champs fautifs, jamais les valeurs soumises.
503{ "error": "Metric registry not available", "code": "METRIC_REGISTRY_LOAD_FAILED" }definitions.json est présent mais mal formé ou illisible.

Les modifications apportées à definitions.json sont prises en compte dès la requête suivante — aucun redémarrage du serveur n'est nécessaire. De même, un fichier jusque-là mal formé redevient fonctionnel dès la requête suivant sa correction.

Utilisation côté frontend

useAnalyticsQuery

Hook React qui s'abonne à une requête analytique via SSE et en renvoie le dernier résultat.

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

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

Type de retour :

{
  data: T | null; // résultat de la requête (tableau typé pour JSON, TypedArrowTable pour ARROW)
  loading: boolean; // true pendant l'exécution de la requête
  error: string | null; // message d'erreur, ou null en cas de succès
  warehouseStatus: WarehouseStatus | null; // voir « Disponibilité du warehouse » ci-dessous
}

Options :

OptionTypeDéfautDescription
format"JSON" | "ARROW""JSON"Format de la réponse
maxParametersSizenumber102400Taille max. des paramètres sérialisés, en octets
autoStartbooleantrueDémarrer la requête au montage

Disponibilité du warehouse

Si le SQL warehouse configuré est STOPPED ou STARTING au moment où une requête est demandée, le plugin Analytics :

  1. Démarre automatiquement le warehouse (lorsqu'il est STOPPED).
  2. Interroge l'état du warehouse et diffuse des événements warehouse_status via SSE jusqu'à ce qu'il passe à RUNNING.
  3. Exécute l'instruction SQL.

Un démarrage à froid ne fige donc plus l'interface sur un indicateur de chargement bloqué. useAnalyticsQuery et useMetricView exposent tous deux le dernier statut de la requête en cours via warehouseStatus ; affichez-le pour informer les utilisateurs :

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>{/* afficher les données */}</table>;
}

Pour les deux hooks, warehouseStatus repasse à null au démarrage d'une requête et le reste jusqu'à l'arrivée du premier événement de statut. Une fois que le serveur a observé le warehouse à l'état RUNNING, les requêtes suivantes effectuées dans les ~30 s ignorent complètement la vérification de disponibilité et warehouseStatus reste à null : en régime établi, le chemin critique n'est donc pas pénalisé par des allers-retours supplémentaires.

Si le warehouse est DELETED/DELETING ou n'atteint pas l'état RUNNING dans le délai configuré, la route émet un événement error (exposé via le champ error).

Indicateur global de disponibilité

Pour les tableaux de bord comportant de nombreux graphiques, un indicateur de chargement par composant ne suffit pas : réintégrer la même interface « warehouse en cours de démarrage » dans chaque squelette devient vite répétitif. AppKit fournit un petit contexte générique (ResourceStatusProvider) et un indicateur prêt à l'emploi (ResourceStatusIndicator) dans lesquels n'importe quel plugin peut publier ; les warehouses analytiques y sont raccordés automatiquement.

L'indicateur affiche le statut en attente le plus critique sous forme de notification sonner : il hérite donc des animations, du thème et de l'empilement de sonner. Le composant monte son propre <Toaster /> (en haut à droite par défaut) et lui transmet ses propriétés (position, theme, richColors, etc.) :

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

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

useAnalyticsQuery et useMetricView s'enregistrent automatiquement auprès du provider le plus proche : aucun câblage par graphique n'est donc nécessaire. Tant que toutes les ressources sont opérationnelles, l'indicateur se contente de rendre le point de montage <Toaster /> ; il affiche un unique toast persistant — toast.loading pour les démarrages à froid, toast.error pour les états irrécupérables — indexé sur le type le plus critique, puis le referme une fois que tout est stabilisé. Comme le même provider est partagé entre les types de ressources (warehouse, lakebase, Model Serving, …), un seul indicateur suffit pour tous les plugins.

Si vous affichez déjà votre propre <Toaster /> pour des toasts applicatifs sans rapport, supprimez l'indicateur et appelez plutôt useResourceStatusToaster() afin que les toasts d'état des ressources passent par ce Toaster unique :

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

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

Pour un contenu de toast entièrement personnalisé, passez render (rendu 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>
  )}
/>

Pour remplacer le texte d'un type précis sans réécrire toute l'interface, passez renderers :

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

Ou construisez votre propre interface à partir de l'agrégat avec useResourceStatus() :

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

// Pire état, tous types confondus
const aggregate = useResourceStatus();
// Uniquement les warehouses
const warehouseOnly = useResourceStatus({ kind: "warehouse" });
// { worst, byKind, affectedLabels, activeCount, elapsedMs }

Le provider est optionnel. Les applications qui ne le montent pas disposent tout de même du champ warehouseStatus propre à chaque hook, et le hook fonctionne exactement comme auparavant.

Publier l'état de vos propres ressources

Les plugins (ou votre propre code) peuvent se brancher sur ce même provider pour des ressources non analytiques — par exemple une connexion Lakebase Postgres en cours de préchauffage, ou un serving endpoint de modèle en démarrage à froid :

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]);
}

Configuration serveur (dans analytics({...})) :

OptionTypePar défautDescription
warehouseStartupTimeoutMsnumber300000 (5 min)Temps d'attente maximal pour que le warehouse passe à l'état RUNNING avant que la requête n'échoue
autoStartWarehousebooleantrueSi true, un warehouse STOPPED démarre automatiquement à la première requête. Définissez false pour les déploiements à coûts maîtrisés, où les démarrages facturables de warehouse ne doivent pas être déclenchés par les requêtes des utilisateurs ; dans ce cas, STOPPED remonte sous la forme d'une ConfigurationError

Exemple avec gestion du chargement, des erreurs et des résultats vides :

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>
  );
}

Requêtes typées

Complétez l'interface QueryRegistry pour bénéficier d'une inférence de types complète sur les paramètres et les résultats :

// 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 }>;
    };
  }
}

Consultez Génération de types pour la génération automatique à partir de fichiers SQL.

Mémoïsation

Encapsulez toujours les paramètres dans useMemo afin d'éviter les boucles de requêtes. Le hook se ré-exécute dès que la référence des paramètres change :

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

// À éviter - crée un nouvel objet à chaque rendu, ce qui provoque des refetchs en boucle
const { data } = useAnalyticsQuery("users", { status: sql.string("active") });

useMetricView

Hook React qui mesure une vue métrique via SSE — le pendant côté client de POST /api/analytics/metric/:key. Plutôt que d'écrire du SQL, vous transmettez les mesures, les dimensions et le filtre sous forme de requête structurée ; le hook renvoie en flux les lignes avec des noms de colonnes typés, ainsi que des métadonnées d'affichage pour chaque colonne.

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" }],
  });

Lorsque "revenue" est une clé du MetricRegistry généré (voir Types de vues de métriques), les noms de mesures/dimensions, les valeurs timeGrain autorisées et les clés de lignes sélectionnées sont tous inférés — passer une mesure inconnue provoque une erreur de type. JSON_ARRAY conserve les cellules scalaires SQL sous forme de chaînes et autorise la valeur SQL NULL pour chaque colonne ; data est donc typé Array<{ arr: string | null; mrr: string | null; created_at: string | null }> | null ; utilisez metadata[col].type lorsque vous analysez volontairement une valeur.

Les queries de séries temporelles doivent trier explicitement leur dimension temporelle sélectionnée par ordre croissant, comme ci-dessus. En dehors de cela, l'ordre des résultats SQL n'est pas garanti ; les utilitaires de graphiques peuvent normaliser les données chronologiques par précaution, mais les consommateurs ne doivent pas s'y fier pour l'ordonnancement des queries.

Options :

OptionTypeRequisDescription
measuresstring[]ouiMesures à agréger. Inférées depuis MetricRegistry[key].measureKeys pour une clé connue.
dimensionsstring[]nonDimensions de regroupement. Inférées depuis measureKeys / dimensionKeys.
filterMetricFilternonArbre de prédicats récursif (même grammaire que la route — voir Filtres).
timeGrainstringnonRegroupe une dimension temporelle en intervalles (day, month, …). Nécessite timeDimension. timeGrains inféré.
timeDimensionstringnonLa dimension unique découpée en intervalles par timeGrain. Doit faire partie de dimensions.
orderBy{field, direction?}[]nonClés de tri. field est restreint aux mesures/dimensions sélectionnées par cet appel ; trier sur une colonne non sélectionnée provoque donc une erreur de type. Voir Résultats déterministes avec limit.
limitnumbernonNombre entier positif limitant les lignes.
autoStartbooleannonDémarre automatiquement la query de métrique. Vaut true par défaut ; définissez false pour la différer jusqu'à ce que l'option passe à true.

Type de retour :

{
  data: T | null; // clés des lignes sélectionnées avec des valeurs JSON_ARRAY string | null
  loading: boolean; // true pendant l'exécution de la requête de métrique
  error: string | null; // message lisible et assaini, ou null en cas de succès
  errorCode: string | null; // code amont stable (basez vos branchements dessus, pas sur le message)
  metadata: Record<string, MetricViewColumnDisplay> | undefined; // métadonnées d'affichage par colonne (voir ci-dessous)
  warehouseStatus: WarehouseStatus | null; // dernier état de disponibilité pour la requête en cours
}

Comme pour useAnalyticsQuery, l'objet d'options est sérialisé en interne (JSON.stringify) : les littéraux d'objet ou de tableau recréés à chaque rendu ne déclenchent donc pas de nouvelle requête tant qu'ils produisent la même chaîne — inutile d'appliquer useMemo aux options. (Il s'agit bien d'une identité de sérialisation, et non d'une égalité structurelle profonde : réordonner les clés dans filter modifie la chaîne et relance donc la requête. Remonter measures/dimensions à la portée du module ou les mémoïser reste tout à fait valable, et conserve les tableaux restreints à leur type de tuple littéral.)

metadata contient les métadonnées d'affichage par colonne uniquement pour les colonnes que vous avez interrogées, délimitées et transportées dans la charge utile result du SSE. Sa valeur est undefined lorsque le serveur n'a résolu aucune métadonnée (clé de métrique inconnue ou types non générés) — traitez-la donc toujours comme optionnelle.

Métadonnées

La route de métrique ajoute à chaque message result des métadonnées d'affichage par colonne (display_name, format, type, description). Ces métadonnées sont générées au build par le générateur de types de vues de métriques, qui les écrit dans config/metric-views/metadata.generated.json, à côté de votre fichier definitions.json rédigé à la main.

Aucune configuration requise. Le plugin découvre le bundle de la même façon qu'il découvre definitions.json ; analytics({}) suffit donc :

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

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

Versionnez metadata.generated.json en même temps que vos types générés : il constitue le volet runtime de la même passe de génération, et la route le lit sur le disque au moment de la requête.

Il s'agit d'une simple décoration de la réponse : les métadonnées n'entrent jamais dans la clé de cache et ne modifient jamais le SQL. Chaque message result de métrique porte un champ metadata limité aux colonnes demandées ; en l'absence de bundle, le message est identique octet pour octet à un résultat /query classique et le metadata du hook vaut undefined. Un bundle manquant ou mal formé se dégrade en colonnes sans libellé et consigne un avertissement dans le journal — il ne fait jamais échouer la requête. Comme les métadonnées voyagent avec la charge utile, le client n'a jamais besoin d'importer le fichier généré ni de coder en dur une chaîne de format : elles sont portées par la charge utile et indépendantes du client.

Pour contourner complètement le fichier — par exemple une application qui construit ses métadonnées autrement, ou qui les fige délibérément — passez analytics({ metricViewsMetadata }). Une valeur explicite l'emporte toujours sur le bundle découvert.

Utilitaires de formatage

@databricks/appkit-ui/js fournit de petits formateurs purs et compatibles avec le tree-shaking qui transforment les valeurs brutes et les métadonnées ci-dessus en chaînes d'affichage. Ils reçoivent la spécification de format (ou MetricViewColumnDisplay) en arguments — sans React, sans dépendance à une bibliothèque de graphiques — et fonctionnent donc aussi bien dans les tableaux et les infobulles que dans les configurations de graphiques.

FonctionRôle
formatValue(value, format?)Formate une valeur brute avec une spécification de format UC/tableur ("$#,##0.00", "#,##0", "0.0%"). Sans spécification → format par défaut pertinent.
formatLabel(name, columnMeta?)Libellé lisible d'une colonne : privilégie columnMeta.display_name, sinon rend le nom brut lisible.
toD3Format(format?)Décompose un format UC en un specifier d3-format et un prefix littéral de devise.

La règle d'or : récupérez toujours le format depuis metadata, ne le saisissez jamais à la main. Lorsque metadata vaut undefined, metadata?.[col]?.format vaut undefined et formatValue bascule proprement sur une valeur par défaut :

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) => (
            // Texte d'en-tête issu de display_name (ou valeur de repli lisible).
            <th key={col}>{formatLabel(col, metadata?.[col])}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data?.map((row, i) => (
          <tr key={i}>
            {columns.map((col) => (
              // La chaîne de format vient des métadonnées, jamais saisie à la main.
              <td key={col}>{formatValue(row[col], metadata?.[col]?.format)}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Transmettre le format aux graphiques

Comme metadata[col].format n'est qu'une simple chaîne de caractères dans la charge utile, la même spécification pilote les graduations des axes et les infobulles, quelle que soit la bibliothèque de graphiques utilisée.

Graphiques AppKit — passez un valueFormatter au graphique intégré. Le second argument correspond au champ de mesure : un seul callback suffit donc à sélectionner le format du catalogue pour chaque série. Le graphique l'applique à son axe de valeurs intégré et aux infobulles de chaque série, sans remplacer les valeurs par défaut internes d'ECharts pour yAxis ou tooltip :

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)
      }
    />
  );
}

Lorsque plusieurs séries partagent un même axe des valeurs, ses graduations utilisent le premier yKey ; chaque infobulle utilise le champ de série correspondant.

La propriété selected n'ajoute une emphase déclarative qu'aux graphiques à barres, en camembert et en anneau. Les graphiques en courbes, en aires, en nuage de points, les cartes thermiques et les radars l'ignorent, car la sémantique de sélection par catégorie n'est pas définie pour ces types de graphiques.

Plotly — transmettez le spécificateur numérique via tickformat et le symbole monétaire littéral via tickprefix. Il est nécessaire de les séparer, car le marqueur $ de d3 dépend des paramètres régionaux et ne peut pas représenter des symboles arbitraires :

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 — utilisez la spécification de format dans 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} />;
}

Dans les deux cas, la chaîne de format provient des metadata injectées par le serveur et n'est jamais écrite en dur dans le composant : il suffit de modifier l'attribut YAML format de la vue métrique pour répercuter le changement sur chaque axe, infobulle et cellule de tableau, sans aucune modification côté client.

Databricks Developer Hub

Prêt à lancer votre prochaine application agentique en quelques minutes ?

Lire la documentation