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
.sqldansconfig/queries/ - La clé de la requête correspond au nom du fichier sans
.sql(par exemplespend_summary.sql→"spend_summary")
Contexte d'exécution
queryKey.sqls'exécute en tant que service principal (cache partagé)queryKey.obo.sqls'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 :limitLIMIT / OFFSET exigent spécifiquement le type Spark IntegerType — BIGINT
(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,BINARYINT,BIGINT,TINYINT,SMALLINT— liaison viasql.int()/sql.bigint()FLOAT,DOUBLE— liaison viasql.float()/sql.double()NUMERIC,DECIMAL— liaison viasql.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 = :workspaceIdEndpoints HTTP
Le plugin Analytics expose les endpoints suivants (montés sous /api/analytics) :
POST /api/analytics/query/:query_keyGET /api/analytics/arrow-result/:jobIdPOST /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 JSONformat: "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 :
| Champ | Type | Requis | Description |
|---|---|---|---|
measures | string[] | oui | Mesures à agréger. Au moins 1, au maximum 50. Chacune devient MEASURE(<name>) AS <name>. |
dimensions | string[] | non | Dimensions de regroupement (20 au maximum). Sélectionnées telles quelles et regroupées via GROUP BY ALL. |
filter | objet | non | Arbre de prédicats structuré traduit en clause WHERE paramétrée (voir Filtres). |
timeGrain | string | non | Regroupe une dimension temporelle via date_trunc('<grain>', …) — par exemple day, month. Nécessite timeDimension. |
timeDimension | string | non | L'unique dimension regroupée par timeGrain. Doit figurer dans dimensions. Obligatoire dès que timeGrain est défini. |
orderBy | tableau | non | Tableau 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. |
limit | number | non | Nombre entier positif limitant les lignes (100000 au maximum). |
format | string | non | JSON_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 100Le 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érateur | SQL | Valeurs |
|---|---|---|
equals | = | exactement une |
notEquals | <> | exactement une |
in | IN (…) | une ou plusieurs |
notIn | NOT IN (…) | une ou plusieurs |
gt / gte / lt / lte | > / >= / < / <= | exactement une |
contains | LIKE :param | exactement une chaîne |
notContains | NOT LIKE :param | exactement une chaîne |
set | IS NOT NULL | aucune |
notSet | IS NULL | aucune |
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 :
executor | S'exécute en tant que | Cache |
|---|---|---|
app_service_principal (par défaut) | Le service principal de l'application | Partagé entre tous les utilisateurs |
user | L'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
| Statut | Corps | Cas |
|---|---|---|
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 :
| Option | Type | Défaut | Description |
|---|---|---|---|
format | "JSON" | "ARROW" | "JSON" | Format de la réponse |
maxParametersSize | number | 102400 | Taille max. des paramètres sérialisés, en octets |
autoStart | boolean | true | Dé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 :
- Démarre automatiquement le warehouse (lorsqu'il est
STOPPED). - Interroge l'état du warehouse et diffuse des événements
warehouse_statusvia SSE jusqu'à ce qu'il passe àRUNNING. - 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({...})) :
| Option | Type | Par défaut | Description |
|---|---|---|---|
warehouseStartupTimeoutMs | number | 300000 (5 min) | Temps d'attente maximal pour que le warehouse passe à l'état RUNNING avant que la requête n'échoue |
autoStartWarehouse | boolean | true | Si 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 :
| Option | Type | Requis | Description |
|---|---|---|---|
measures | string[] | oui | Mesures à agréger. Inférées depuis MetricRegistry[key].measureKeys pour une clé connue. |
dimensions | string[] | non | Dimensions de regroupement. Inférées depuis measureKeys / dimensionKeys. |
filter | MetricFilter | non | Arbre de prédicats récursif (même grammaire que la route — voir Filtres). |
timeGrain | string | non | Regroupe une dimension temporelle en intervalles (day, month, …). Nécessite timeDimension. timeGrains inféré. |
timeDimension | string | non | La dimension unique découpée en intervalles par timeGrain. Doit faire partie de dimensions. |
orderBy | {field, direction?}[] | non | Clé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. |
limit | number | non | Nombre entier positif limitant les lignes. |
autoStart | boolean | non | Dé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.
| Fonction | Rô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.