Incorporare una dashboard
Incorporare è il momento in cui una dashboard smette di essere qualcosa che il tuo team apre e diventa parte di un prodotto che i tuoi clienti usano. Vibedasher ha due modi per farlo, e la differenza non è cosmetica — decide chi può leggere i dati.
| Corsia pubblica | Corsia a token firmato | |
|---|---|---|
view_mode della dashboard | public | protected |
| Chi può visualizzare | chiunque abbia l'URL | solo un visualizzatore per cui il tuo backend ha generato un token |
| Filtraggio per riga per singolo visualizzatore | no | sì, applicato lato server |
| Cosa incorpori | un URL | un URL più un token di breve durata |
Una dashboard private non è incorporabile da nessuna delle due corsie. Entrambe le
corsie richiedono che la dashboard sia prima pubblicata — vedi
Pubblica e condividi.
Incorporare mette la nostra dashboard renderizzata dentro un rettangolo della tua pagina. L'altra risposta è espellere — prendere i sorgenti React e SQL e renderizzarli tu, in modo nativo, con noi fuori dal percorso di rendering e solo il motore dati sottostante. Quale delle due vuoi lo decidono soprattutto due domande: visualizzatori diversi devono vedere righe diverse (incorporare è la sola corsia che lo fa), e la dashboard deve avere l'aspetto e il comportamento del resto della tua app (solo l'espulsione te lo dà). I compromessi sono messi a confronto in Incorporare o espellere.
La corsia pubblica
Se la dashboard è pubblicata con view_mode: public, un iframe è l'intera integrazione:
<iframe
src="https://cloud.vibedasher.com/v/<slug>"
width="100%"
height="800"
style="border: 0;"
title="Vibedasher dashboard"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
loading="lazy"
></iframe>
allow-same-origin sembra allarmante accanto ad allow-scripts ed è sicuro qui
specificamente perché il frame è cross-origin rispetto alla tua pagina: concede al
renderer la propria origine, non la tua. allow-top-navigation è deliberatamente
assente, quindi il frame non può far navigare via la tua pagina. allow-downloads è ciò
che fa funzionare l'esportazione PNG dei grafici.
Lo snippet funziona da qualunque pagina tu controlli — dalla nostra parte non c'è nulla da configurare prima. L'audience sotto cui pubblichi è il controllo che conta qui, e Il confine del frame è dove quella scelta viene esposta.
La finestra di dialogo Condividi nell'app ti fornisce questo snippet già precompilato. Prendilo da lì invece di assemblare l'URL a mano.
La corsia a token firmato
Questa è quella che vale la pena capire, perché è l'unico posto in cui Vibedasher applica la sicurezza per riga.
Il tuo backend genera il token — tu non firmi nulla
Vibedasher firma il token stesso, ES256, con una propria chiave rotante. Il tuo backend ne richiede uno con la sua chiave API:
from vibedasher.models import EmbedTokenRequest, EmbedTokenRequestFilters
resp = client.embed_tokens.create_embed(
EmbedTokenRequest(
viz_id=1234,
filters=EmbedTokenRequestFilters.from_dict({"region": "EMEA"}),
ttl_seconds=600,
)
)
print(resp.token, resp.expires_in)
const resp = await client.embedTokens.createEmbed({
vizId: 1234,
filters: { region: 'EMEA' },
ttlSeconds: 600,
});
Non possiedi alcun materiale di firma e non ne hai bisogno — genera i token come mostrato qui.
Oppure in HTTP semplice, da qualsiasi linguaggio:
curl -X POST https://api.vibedasher.com/api/v1/embed/token \
-H "X-Api-Key: $VIBEDASHER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vizId": 1234, "ttlSeconds": 600, "filters": {"region": "EMEA"}}'
# → {"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600}
Il corpo della richiesta accetta:
| Campo | Obbligatorio | Note |
|---|---|---|
vizId | sì | Esattamente una dashboard. Un token non può coprire una cartella o un account. |
filters | no | I filtri per riga. Vedi sotto. |
viewerId | no | Un id di posto opaco. Solo per audit e metering — non è un input di controllo degli accessi. Non farci affidamento per delimitare nulla. |
ttlSeconds | no | Predefinito 600. Minimo 60, massimo rigido 86 400 (24 h). I valori fuori intervallo vengono limitati alla generazione e riverificati alla verifica. La verifica ammette un piccolo margine di scarto dell'orologio oltre exp, limitato a 120 s e alla metà della durata di vita del token stesso: la durata effettivamente applicata non supera mai 1,5× expires_in e, da un TTL di 240 s in su, il margine è il valore fisso di 120 s. |
apiKeyId | no | Necessario solo quando si genera da una sessione browser, per vincolare il token alla lista consentita di domini di una chiave e alla sua revoca. |
L'account_id viene impresso dalla tua credenziale e non viene mai letto dal corpo.
La generazione fallisce con un messaggio che nomina il controllo fallito — stai generando un token, non presentandolo, quindi l'errore ti dice cosa cambiare nella dashboard o nella chiave:
| Stato | Dettaglio | Rimedio |
|---|---|---|
403 | la visualizzazione appartiene a un altro account | genera con una chiave API dell'account proprietario |
403 | la visualizzazione non è incorporabile con un token: la sua visibilità deve essere protected | imposta la visibilità della dashboard su Protetta nella finestra Condividi |
404 | la visualizzazione non è pubblicata | pubblicala prima |
403 | nessuna chiave API utilizzabile a cui legare il token | la chiave manca, è inattiva, scaduta o appartiene a un altro account |
404 | Viz non trovata | controlla vizId |
Una dashboard public viene rifiutata da questa corsia — ha già la corsia anonima, e
mescolare le due sarebbe un modo per credere di avere filtri per riga quando non è così.
Filtri per riga
filters è una mappa da nome di colonna a uno scalare o una lista. Uno scalare diventa =,
una lista diventa IN, e più chiavi sono combinate con AND. Le chiavi sono il sqlName
della colonna così come lo riporta GET /v1/datasets/{id} — il nome che la vista SQL espone,
non il nome visualizzato.
La piattaforma non aggiunge una clausola WHERE alla tua query; riscrive ogni riferimento
a un dataset in una relazione filtrata, mantenendo l'alias originale:
FROM orders
-> FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders
È questo che rende l'ambito non allargabile, e non semplicemente presente. Ciò che la
pagina chiede si applica all'esterno della relazione filtrata, quindi può solo restringerla
ulteriormente — WHERE tenant = 'globex' OR 1=1 vede comunque un solo tenant, una
sottoquery avvolgente avvolge la relazione filtrata, entrambi i rami di una UNION vengono
riscritti, e una CTE che maschera l'alias, un riferimento qualificato da schema e qualsiasi
funzione che legga file sono rifiutati senza appello.
Tre proprietà lo rendono verificabile:
- I filtri provengono solo dal token verificato. Non vengono mai letti dal corpo della richiesta, quindi uno spettatore che curiosa nella scheda di rete non può allargare il proprio ambito.
- Una chiave di filtro che non nomina alcuna colonna dei dataset della dashboard viene
rifiutata all'emissione —
403, nominando la chiave ed elencando i nomi di colonna che esistono davvero. Nessun token viene firmato. Lo stesso vale per un valore che non può essere convertito nel tipo della sua colonna. Un filtro che corrispondesse silenziosamente a tutto sarebbe una fuga di dati travestita da controllo di sicurezza: viene quindi rifiutato prima di poter diventare un link che consegni a un cliente. - L'emissione ti dice cosa permette il token, nel campo
scopedella sua risposta. Leggilo; non darlo per scontato.
Leggi scope.fullyScoped — non significa «i filtri sono stati accettati»
Una dashboard può leggere più di un dataset, e un filtro può applicarsi solo a un dataset che
possiede quella colonna — non si può filtrare su una colonna che una tabella non ha.
Così una dashboard su orders e price_list, con ambito {"tenant": "acme"}, filtra
orders e legge price_list per intero. La pagina si carica, ogni riquadro si disegna,
ogni numero è plausibile, e nella richiesta non c'è nulla che appaia sbagliato.
È corretto e voluto quando il secondo dataset è una tabella di lookup condivisa — un listino
prezzi, un catalogo di piani, un insieme di etichette. Non è ciò che vuoi quando il
secondo dataset contiene righe per cliente sotto una colonna con un nome diverso (tenant_id
invece di tenant, customer, org). La risposta dell'emissione è dove scopri in quale dei
due casi ti trovi:
{
"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600,
"scope": {
"vizId": 1234,
"filtered": [{"datasetId": 7, "datasetName": "orders",
"predicate": "\"tenant\" = 'acme'", "columns": ["tenant"]}],
"unfiltered": [{"datasetId": 8, "datasetName": "price_list",
"predicate": null, "columns": []}],
"unresolvedDatasetIds": [],
"fullyScoped": false,
"meaning": "PARTIALLY SCOPED — read this before handing the link out. ..."
}
}
| Campo | Cosa farne |
|---|---|
fullyScoped | Quello su cui asserire. true solo quando ogni dataset che la dashboard legge è delimitato da questi filtri. Tutto il resto è false. |
filtered[].predicate | L'SQL esatto applicato a quel dataset. È la frase da mettere davanti a un revisore di sicurezza. |
unfiltered[] | Dataset che questo token legge per intero. Decidi, per ciascuno, se sia una tabella condivisa o una falla. |
unresolvedDatasetIds | Dataset il cui schema non è stato leggibile. Non è la stessa cosa di «non filtrato»: «legge tutto» e «non sappiamo cosa legga» sono risposte diverse, e nessuna viene fusa nell'altra. |
Un controllo di produzione ragionevole nella tua route di emissione è rifiutare di
restituire il token quando scope.fullyScoped è false e ti aspettavi true — tu sai quali
delle tue dashboard debbano essere per cliente, la piattaforma no.
Presenta il token
Il frame non può impostare un header, quindi il token viaggia sull'URL:
<iframe
src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
></iframe>
Le risposte di embed vengono inviate con Referrer-Policy: no-referrer in modo che il
token non trapeli in un header Referer sulle sotto-richieste. Resta comunque una
credenziale bearer in un URL, che è la vera ragione per cui il TTL predefinito è di
dieci minuti: genera un token per ogni caricamento di pagina, tienilo breve, e rinnovalo
invece di alzare il TTL.
Configura la tua lista consentita di domini
Una chiave API può portare con sé un elenco di domini da cui i suoi token di embed
saranno onorati. Un elenco vuoto significa consenti-tutto. È una decisione
deliberata affinché la funzionalità funzioni nel momento in cui la provi, e significa
che una chiave non configurata non protegge nulla a livello di origine. Imposta
allowed_domains sulla chiave prima di andare in produzione.
Il ciclo di rinnovo del token
Un token di dieci minuti su una dashboard che qualcuno lascia aperta tutto il pomeriggio deve essere rinnovato, e il renderer ti dice quando:
- Il frame invia
EMBED_TOKEN_EXPIRED(oEMBED_TOKEN_INVALID) alla tua pagina. - La tua pagina chiede al tuo backend un nuovo token.
- La tua pagina invia
UPDATE_TOKENnel frame:{ type: 'UPDATE_TOKEN', token: '<new>' }.
window.addEventListener('message', async (e) => {
if (e.origin !== 'https://cloud.vibedasher.com') return;
if (e.data?.type === 'EMBED_TOKEN_EXPIRED' || e.data?.type === 'EMBED_TOKEN_INVALID') {
const token = await fetch('/api/vibedasher-embed-token').then((r) => r.text());
frame.contentWindow.postMessage({ type: 'UPDATE_TOKEN', token }, 'https://cloud.vibedasher.com');
}
});
Controlla sempre e.origin. UPDATE_TOKEN è l'unico messaggio che un host di terze
parti può inviare verso l'interno — tutto il resto viene ignorato.
Cosa riceve realmente la tua pagina
I messaggi che un host di terze parti riceve sono un elenco fisso e consentito di tredici. Questo è l'elenco completo:
| Messaggio | Quando |
|---|---|
READY | Il renderer si è avviato. |
LOADED | La dashboard ha finito di caricarsi. |
RESIZE | L'altezza del contenuto è cambiata — usalo per dimensionare l'iframe. |
BUNDLE_STARTED / BUNDLE_COMPLETED / BUNDLE_FAILED / BUNDLE_RETRY | Ciclo di vita della build del codice della dashboard. |
RUNTIME_ERROR | Il codice della dashboard ha generato un'eccezione. |
EMPTY_DATA | Una query non ha restituito righe. |
RENDER_HEALTH | Stato di salute del rendering per singolo tile. |
EMBED_TOKEN_EXPIRED / EMBED_TOKEN_INVALID / EMBED_UNAVAILABLE | Ciclo di vita del token — vedi sopra. |
Qualsiasi altra cosa tu possa vedere menzionata è interna. In particolare un insieme di
messaggi non viene esplicitamente mai inoltrato a un host di terze parti —
VIZ_DATA, FILES, UPDATE_TOKEN in uscita, SQL_ERROR, CONSOLE_ERROR,
REQUEST_NEW_AUTH_TOKEN, ELEMENT_SELECTED, FILTER_STATE_CHANGED — perché portano o
il sorgente della tua dashboard o il suo stato grezzo di query.
Il confine del frame
Due proprietà decidono su cosa può essere costruita un'integrazione, ed entrambe vale la pena leggerle prima di progettare attorno al frame, non dopo.
Il frame possiede il proprio stato di filtraggio. I messaggi che un host terzo riceve sono quelli elencati sopra; lo stato di filtraggio della dashboard non è fra loro, in nessuna delle due direzioni. Il frame è una vista autonoma: guidalo con l'URL e il token che emetti, e lascia che possieda ciò che accade all'interno. Se la tua interfaccia deve condividere una selezione con la dashboard, metti quella selezione nel predicato di filtraggio del token, che la piattaforma aggiunge a ogni query che autorizza — lato server, e con la stessa risposta per ogni tile.
Scegli l'audience pensando al framing. Una dashboard pubblicata come public è un URL
pubblico, con tutto quello che ne segue; la corsia a token firmato è quella da usare quando
conta chi guarda e da dove, e l'elenco dei domini consentiti di una chiave API è ciò che lega
le sue risposte alla tua origine.
La corsia di query a token firmato gira in eu-central-1. È lì che vive il runtime di
query per gli embed protetti. Se i dati del tuo account sono in un'altra regione, parlane con
noi prima di costruire su questa corsia.
Passi successivi
- Chi può vedere cosa — dove si inserisce il filtraggio per riga nel modello di permessi più ampio, e perché esiste solo qui.
- Pubblica e condividi — portare una dashboard a
publishede scegliere il suoview_mode. - Riferimento API — l'operazione
embed-tokensper intero.