Des alertes qu'un agent peut écrire
Un tableau de bord vous dit quelque chose quand vous l'ouvrez. Une alerte vous prévient quand vous ne regardiez pas — et c'est justement la partie qu'un harness ne peut pas produire tout seul : il lui faut un état hébergé, des données fraîches, une horloge murale, et un endroit où livrer.
Une alerte, ce sont cinq décisions, donc l'API a cinq champs :
| Champ | Ce qu'il décide |
|---|---|
when | Le SQL dont les lignes sont l'anomalie |
say | La phrase que lit le destinataire, remplie depuis la première ligne |
every | À quelle fréquence la condition est regardée |
capture | Quelle image est prise, en Playwright |
to | Qui la reçoit |
L'exemple complet
from vibedasher import Vibedasher
client = Vibedasher()
alert = client.alerts.create(
viz_id=1430,
name="Margin watch",
when="""SELECT region, ROUND(margin*100,1) AS margin_pct
FROM margins WHERE margin < 0.10 ORDER BY margin LIMIT 5""",
say="Profit margin {margin_pct}% is below 10% in {region}",
every="0 7 * * *",
capture="""await page.goto('?f.region={region}');
await expect(page.getByTestId('chart-rendered')).toBeVisible();
await page.locator('[data-capture-root]').screenshot();""",
to=["finance@acme.com"],
)
client.alerts.verify(alert.id) # chaque sélecteur se résout — ne capture rien, n'envoie rien
client.alerts.test(alert.id) # capture pour de vrai ET ENVOIE UN MAIL à finance@acme.com
Le même appel en TypeScript, où le SDK est en camelCase :
const alert = await client.alerts.create({
vizId: 1430,
name: 'Margin watch',
when: 'SELECT region, ROUND(margin*100,1) AS margin_pct FROM margins WHERE margin < 0.10 ORDER BY margin LIMIT 5',
say: 'Profit margin {margin_pct}% is below 10% in {region}',
every: '0 7 * * *',
capture: "await page.goto('?f.region={region}');\nawait expect(page.getByTestId('chart-rendered')).toBeVisible();\nawait page.locator('[data-capture-root]').screenshot();",
to: ['finance@acme.com'],
});
await client.alerts.verify(alert.id); // chaque sélecteur se résout — ne capture rien, n'envoie rien
await client.alerts.test(alert.id); // capture pour de vrai ET ENVOIE UN MAIL à finance@acme.com
En MCP, c'est create_alert avec les cinq mêmes noms, puis verify_alert et
test_alert.
Le contrat « truthy »
Zéro ligne signifie non déclenchée. Une ligne ou plus signifie déclenchée. Il n'y a
pas de comparateur à inverser par erreur ni de seuil à garder synchronisé avec la requête :
la clause WHERE est la condition.
La première ligne renvoyée constitue les métadonnées du message, et say la transforme en
mots. Écrivez l'anomalie et son explication comme une seule requête, pour que la phrase ne
puisse rien affirmer que les données ne disent pas :
SELECT region,
ROUND(margin*100,1) AS margin_pct,
10 AS threshold_pct
FROM margins
WHERE margin < 0.10
ORDER BY margin
Ce que le destinataire lit (la phrase est produite par le programme, elle reste donc en anglais) :
Profit margin 8.2% is below 10% in West, and 3 more
Les lignes supplémentaires sont du contexte, plafonnées à cinq. Un espace réservé de say
qui nomme une colonne que la requête ne SELECT pas est refusé au moment où vous créez
l'alerte, avec la colonne nommée et la liste de celles qui sont disponibles — jamais un
blanc, et jamais le littéral {region} envoyé par mail à quelqu'un comme s'il s'agissait
d'un texte que vous auriez écrit. Les alias comptent : c'est
ROUND(margin*100,1) AS margin_pct qui fait que {margin_pct} se résout.
L'ancienne forme — comparator plus threshold sur une seule cellule numérique — reste
acceptée comme sucre syntaxique. Préférez la forme truthy : « zéro ligne » est sans
ambiguïté d'une manière que « le nombre n'est pas sous le seuil » n'est pas.
L'image montre l'anomalie, pas la vue par défaut
{column} fonctionne aussi dans capture, et c'est tout l'intérêt. Un espace réservé y est
une valeur : ?f.region={region} épingle le filtre sur la région qui s'est réellement
déclenchée, donc l'alerte sur West est une image de West. Le lien du message est construit à
partir de la même recette, donc l'image et le lien concordent.
capture: await page.goto('?page=overview&f.region={region}');
await expect(page.getByTestId('chart-rendered')).toBeVisible();
await page.locator('[data-capture-root]').screenshot();
getByTestId('<name>') adresse un contrôle par le nom de configuration que vous lui avez
donné — describe_dashboard_controls(viz_id) en MCP, ou GET /v1/viz/{id}/controls,
liste les noms et les valeurs qu'un tableau de bord accepte réellement. Jamais un chemin CSS
brut : ceux-là sont refusés, parce qu'ils cassent silencieusement dès que le balisage bouge.
La recette est analysée, jamais exécutée. page.evaluate, require/import, les
littéraux de gabarit, la concaténation de chaînes, les commentaires et les chemins CSS
reviennent chacun sous forme d'un refus qui nomme la ligne. Ce qui s'exécute est une petite
recette déclarative que la voie de capture applique en construisant l'URL, ce qui est
déterministe d'une manière qu'un clic n'est pas.
every : une chaîne cron ou une durée
every="0 7 * * *" # 07h00 tous les jours
every="15m" # toutes les quinze minutes
every="2h" # toutes les deux heures
every="1d" # une fois par jour à minuit
Seuls les pas qui pavent leur unité de façon régulière sont acceptés. 7m est refusé : cron
redémarre un pas au début de l'heure, donc */7 se déclencherait à :00, :07 … :56 puis de
nouveau quatre minutes plus tard — pas « toutes les sept minutes ». Le refus liste les pas
qui fonctionnent.
Une expression cron valide mais qui ne peut jamais survenir — 0 0 30 2 *, qui demande
le 30 février — est refusée elle aussi. Rien en aval ne s'en plaindrait jamais : l'alerte
resterait active, jamais due, jamais évaluée, et chaque surface la signalerait comme saine
pour toujours.
Omettez every avec when défini et l'alerte est évaluée à la cadence propre de
l'ordonnanceur.
to : le canal se lit sur l'adresse
to=[
"finance@acme.com", # e-mail
"https://hooks.slack.com/services/T/B/X", # webhook entrant Slack
"usr_1a2b3c", # un identifiant d'utilisateur — notification in-app
]
Les trois formes sont disjointes, il n'y a donc aucun canal à déclarer à côté de l'adresse. Quelque chose qui ressemble à une adresse sans être ni l'une ni l'autre est refusé plutôt que classé comme un identifiant d'utilisateur qui n'existe pas.
La garantie cache-zéro
Une condition est toujours exécutée avec le cache SQL contourné. Vous ne le demandez pas et vous ne pouvez pas le désactiver. Une alerte qui se déclencherait sur un résultat en cache ne serait pas une alerte — ce serait le rapport de quelque chose qui a été vrai à un moment donné. Aucune lecture de cache, aucune incrémentation du compteur de hits, aucune écriture de cache : la requête va au moteur à chaque évaluation.
Se déclencher sur la transition, pas à chaque vérification
Entrer dans l'anomalie déclenche une fois. Y rester est délibérément silencieux. L'alerte se réarme quand la condition se résorbe. Une métrique qui oscille autour de sa limite envoie un message, pas un par tick.
Le canari : lancez verify après avoir modifié un tableau de bord
verify est le seul appel qui vous dit que votre recette se résout encore.
result = client.alerts.verify(alert.id)
if not result.verification.resolved:
for step in result.verification.steps:
print(step.index, step.status, step.detail)
Il ne capture rien, n'envoie rien, n'écrit aucune ligne et ne déplace aucune colonne. Il exécute le même exécuteur qu'une vraie capture, en mode résolution seule, contre le code généré du tableau de bord lui-même — il ne peut donc pas dériver de ce que fera la capture. Chaque étape est rapportée, pas seulement la première : renommez trois sélecteurs et on vous en signale trois.
Prenez-en l'habitude, car la panne qu'il attrape est invisible autrement :
Vous modifiez un tableau de bord. Vous renommez un filtre. La recette de l'alerte pointe toujours vers l'ancien nom. Rien n'échoue — jusqu'à ce qu'un destinataire ouvre un e-mail contenant l'image de la mauvaise chose.
Une étape rouge est le changement cassant que vous venez de faire. Lancez verify après
chaque modification de tableau de bord qui touche un identifiant de page, un nom de sélecteur
ou une valeur de filtre, et vous le trouvez pendant que vous pouvez encore le réparer.
Chaque refus nomme le correctif
Un 400 renvoyé par ces endpoints est fait pour être lu et suivi d'effet, pas réessayé (les
messages ci-dessous sont ceux que l'API renvoie, en anglais) :
say= references {margin_pct}, but the when= query does not return that column.
It returns: region. Fix: SELECT it (an alias counts, e.g.
'ROUND(margin*100,1) AS margin_pct'), or reference one of the columns above.
every='7m' does not divide the hour evenly, so it would not actually run every
7 minutes — cron restarts the step at :00, making the last gap of each hour
shorter. Fix: use one of 5m, 6m, 10m, 12m, 15m, 20m, 30m, or pass the cron
expression you mean.
Cron '0 0 30 2 *' is a valid expression that never occurs — no date matches its
day-of-month, month and day-of-week together (e.g. '30 2' asks for February
30th). An alert on it would stay 'active' and silent forever. Fix: name a date
that exists, or use a duration like every='1d'.
L'ordre dans lequel travailler
describe_dashboard_controls(viz_id)— les noms et les valeurs que ce tableau de bord accepte réellement. L'ignorer, c'est deviner les noms de sélecteurs.- Écrivez le SQL. Ses lignes sont l'anomalie ; ses colonnes sont la phrase.
- Écrivez la capture. Utilisez
{column}pour que l'image soit l'anomalie. verify— avant de lui faire confiance, et de nouveau après chaque modification de tableau de bord.test— capture pour de vrai et renvoie la ligne de livraison à surveiller. Il envoie un message aux destinataires de l'alerte : un run de test est un envoi réel, et seule la comptabilité diffère d'un vrai déclenchement (lastFiredAtne bouge pas, parce que rien ne s'est déclenché). Lancez-le quand un vrai message à ces adresses est ce que vous voulez ;verifyest l'étape qui n'atteint personne.
Permissions
Créer, modifier, supprimer et tester une alerte demande le rôle creator ou admin et
un accès en édition au tableau de bord. En lire une — et verify, qui ne dérive rien que
vous ne puissiez déjà lire — demande un accès en lecture. Un tableau de bord que vous ne
pouvez pas modifier répond 404, pas 403 : le refus ne confirme pas que l'identifiant
existe.