Aller au contenu principal

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 :

ChampCe qu'il décide
whenLe SQL dont les lignes sont l'anomalie
sayLa phrase que lit le destinataire, remplie depuis la première ligne
everyÀ quelle fréquence la condition est regardée
captureQuelle image est prise, en Playwright
toQui 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​

  1. 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.
  2. Écrivez le SQL. Ses lignes sont l'anomalie ; ses colonnes sont la phrase.
  3. Écrivez la capture. Utilisez {column} pour que l'image soit l'anomalie.
  4. verify — avant de lui faire confiance, et de nouveau après chaque modification de tableau de bord.
  5. 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 (lastFiredAt ne bouge pas, parce que rien ne s'est déclenché). Lancez-le quand un vrai message à ces adresses est ce que vous voulez ; verify est 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.