Passa al contenuto principale

Avvisi che un agente può scrivere

Una dashboard ti dice qualcosa quando la apri. Un avviso te lo dice quando non stavi guardando — ed è proprio la parte che un harness non può produrre da solo: gli servono stato ospitato, dati freschi, un orologio a muro e un posto dove consegnare.

Un avviso sono cinque decisioni, quindi l'API ha cinque campi:

CampoCosa decide
whenIl SQL le cui righe sono la violazione
sayLa frase che legge il destinatario, riempita dalla prima riga
everyOgni quanto viene guardata la condizione
captureQuale immagine viene scattata, in Playwright
toChi lo riceve

L'esempio svolto​

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) # ogni selettore si risolve — non cattura nulla, non invia nulla
client.alerts.test(alert.id) # cattura davvero E INVIA UNA MAIL a finance@acme.com

La stessa chiamata in TypeScript, dove l'SDK è in 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); // ogni selettore si risolve — non cattura nulla, non invia nulla
await client.alerts.test(alert.id); // cattura davvero E INVIA UNA MAIL a finance@acme.com

Via MCP è create_alert con gli stessi cinque nomi, poi verify_alert e test_alert.

Il contratto «truthy»​

Zero righe significa non scattato. Una riga o più significa scattato. Non c'è nessun comparatore da invertire per sbaglio né alcuna soglia da tenere sincronizzata con la query: la clausola WHERE è la condizione.

La prima riga restituita sono i metadati del messaggio, e say li trasforma in parole. Scrivi la violazione e la sua spiegazione come un'unica query, così che la frase non possa affermare nulla che i dati non dicano:

SELECT region,
ROUND(margin*100,1) AS margin_pct,
10 AS threshold_pct
FROM margins
WHERE margin < 0.10
ORDER BY margin

Ciò che legge il destinatario (la frase la produce il programma, quindi resta in inglese):

Profit margin 8.2% is below 10% in West, and 3 more

Le righe in più sono contesto, con un tetto di cinque. Un segnaposto di say che nomina una colonna che la query non fa SELECT viene rifiutato quando crei l'avviso, con la colonna nominata e l'elenco di quelle disponibili — mai uno spazio vuoto, e mai il letterale {region} spedito per email a qualcuno come se fosse un testo scritto da te. Gli alias contano: è ROUND(margin*100,1) AS margin_pct a far risolvere {margin_pct}.

La forma più vecchia — comparator più threshold su una singola cella numerica — è ancora accettata come zucchero sintattico. Preferisci la forma truthy: «zero righe» è inequivocabile in un modo in cui «il numero non è sotto la soglia» non lo è.

L'immagine mostra la violazione, non la vista predefinita​

{column} funziona anche in capture, ed è tutto il punto. Lì un segnaposto è un valore: ?f.region={region} fissa il filtro sulla regione che è davvero scattata, quindi l'avviso su West è un'immagine di West. Il link nel messaggio è costruito con la stessa ricetta, così immagine e link concordano.

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>') indirizza un controllo tramite il nome di configurazione che gli hai dato — describe_dashboard_controls(viz_id) via MCP, oppure GET /v1/viz/{id}/controls, elenca i nomi e i valori che una dashboard accetta davvero. Mai un percorso CSS grezzo: quelli vengono rifiutati, perché si rompono in silenzio la prossima volta che il markup si sposta.

La ricetta viene analizzata, mai eseguita. page.evaluate, require/import, i template literal, la concatenazione di stringhe, i commenti e i percorsi CSS tornano ciascuno come un rifiuto che nomina la riga. Ciò che viene eseguito è una piccola ricetta dichiarativa che la corsia di cattura applica costruendo l'URL, deterministica in un modo in cui un clic non lo è.

every: una stringa cron o una durata​

every="0 7 * * *" # le 07:00 di ogni giorno
every="15m" # ogni quindici minuti
every="2h" # ogni due ore
every="1d" # una volta al giorno a mezzanotte

Sono accettati solo i passi che riempiono la loro unità in modo esatto. 7m viene rifiutato: cron fa ripartire il passo allo scoccare dell'ora, quindi */7 scatterebbe a :00, :07 … :56 e poi di nuovo quattro minuti dopo — non «ogni sette minuti». Il rifiuto elenca i passi che funzionano.

Anche un'espressione cron valida che non può mai verificarsi — 0 0 30 2 *, che chiede il 30 febbraio — viene rifiutata. Nulla a valle se ne lamenterebbe mai: l'avviso resterebbe active, mai dovuto, mai valutato, e ogni superficie lo segnalerebbe come sano per sempre.

Ometti every con when impostato e l'avviso viene valutato alla cadenza propria dello scheduler.

to: il canale si legge dall'indirizzo​

to=[
"finance@acme.com", # email
"https://hooks.slack.com/services/T/B/X", # webhook in ingresso di Slack
"usr_1a2b3c", # un id utente — notifica in-app
]

Le tre forme sono disgiunte, quindi non c'è nessun canale da dichiarare accanto all'indirizzo. Qualcosa che sembra un indirizzo e non è né l'uno né l'altro viene rifiutato invece di essere archiviato come un id utente che non esiste.

La garanzia cache-zero​

Una condizione viene sempre eseguita bypassando la cache SQL. Non lo chiedi tu e non puoi disattivarlo. Un avviso che scattasse su un risultato in cache non sarebbe un avviso — sarebbe il resoconto di qualcosa che è stato vero a un certo punto. Nessuna lettura di cache, nessun incremento del contatore di hit, nessuna scrittura di cache: la query va al motore a ogni valutazione.

Scattare sulla transizione, non a ogni controllo​

Entrare nella violazione fa scattare una volta. Restarci è deliberatamente silenzioso. L'avviso si riarma quando la condizione rientra. Una metrica che oscilla attorno al suo limite invia un messaggio, non uno per tick.

Il canarino: esegui verify dopo aver modificato una dashboard​

verify è l'unica chiamata che ti dice che la tua ricetta si risolve ancora.

result = client.alerts.verify(alert.id)
if not result.verification.resolved:
for step in result.verification.steps:
print(step.index, step.status, step.detail)

Non cattura nulla, non invia nulla, non scrive alcuna riga e non muove alcuna colonna. Esegue lo stesso executor che esegue una cattura reale, in modalità solo-risoluzione, contro il codice generato della dashboard stessa — quindi non può divergere da ciò che farà la cattura. Ogni passo viene riportato, non solo il primo: rinomina tre selettori e te ne verranno segnalati tre.

Fanne un'abitudine, perché il guasto che intercetta è altrimenti invisibile:

Modifichi una dashboard. Rinomini un filtro. La ricetta dell'avviso punta ancora al vecchio nome. Nulla fallisce — finché un destinatario non apre un'email con dentro l'immagine della cosa sbagliata.

Un passo rosso è la modifica incompatibile che hai appena fatto. Esegui verify dopo ogni modifica alla dashboard che tocca un id di pagina, un nome di selettore o un valore di filtro, e lo trovi finché puoi ancora ripararlo.

Ogni rifiuto nomina la correzione​

Un 400 da questi endpoint è pensato per essere letto e agito, non ritentato (i messaggi qui sotto sono quelli che restituisce l'API, in inglese):

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'ordine in cui lavorare​

  1. describe_dashboard_controls(viz_id) — i nomi e i valori che questa dashboard accetta davvero. Saltarlo significa indovinare i nomi dei selettori.
  2. Scrivi il SQL. Le sue righe sono la violazione; le sue colonne sono la frase.
  3. Scrivi la cattura. Usa {column} così che l'immagine sia la violazione.
  4. verify — prima di fidarti, e di nuovo dopo ogni modifica alla dashboard.
  5. test — cattura davvero e restituisce la riga di consegna da tenere d'occhio. Invia un messaggio ai destinatari dell'avviso: un'esecuzione di test è un invio reale, e solo la contabilità differisce da uno scatto vero (lastFiredAt non si muove, perché non è scattato nulla). Eseguilo quando un messaggio reale a quegli indirizzi è ciò che vuoi; verify è il passaggio che non raggiunge nessuno.

Permessi​

Creare, modificare, eliminare e testare un avviso richiede il ruolo creator o admin e l'accesso in modifica alla dashboard. Leggerne uno — e verify, che non deriva nulla che tu non potessi già leggere — richiede l'accesso in visualizzazione. Una dashboard che non puoi modificare risponde 404, non 403: il rifiuto non conferma che l'id esista.