Scritture di BigQuery utilizzando le azioni di Looker sulle funzioni Cloud Run

Molti clienti di Looker vogliono consentire ai propri utenti di andare oltre la creazione di report sui dati nel data warehouse e di scrivere e aggiornare effettivamente il data warehouse.

Tramite l'API Action, Looker supporta questo caso d'uso per qualsiasi data warehouse o destinazione. Questa pagina di documentazione illustra ai clienti che utilizzano l'infrastruttura Google Cloud come servizio il deployment di una soluzione su Cloud Run Functions per scrivere in BigQuery. Questa pagina tratta i seguenti argomenti:

Considerazioni sulle soluzioni

Utilizza questo elenco di considerazioni per verificare che questa soluzione sia in linea con le tue esigenze.

  • Cloud Run Functions
    • Perché scegliere Cloud Run Functions? In quanto offerta "serverless" di Google, Cloud Run Functions è un'ottima scelta per la facilità di operazioni e manutenzione. Una considerazione da tenere presente è che la latenza, in particolare per le chiamate a freddo, potrebbe essere più lunga rispetto a una soluzione basata su un server dedicato.
    • Linguaggio e runtime Cloud Run Functions supporta più linguaggi e runtime. Questa pagina di documentazione si concentrerà su un esempio in JavaScript e Node.js. Tuttavia, i concetti sono direttamente traducibili negli altri linguaggi e runtime supportati.
  • BigQuery
    • Perché BigQuery? Sebbene questa pagina di documentazione presupponga che tu stia già utilizzando BigQuery, BigQuery è un'ottima scelta per un data warehouse in generale. Tieni presente le seguenti considerazioni:
      • API BigQuery Storage Write (gRPC): BigQuery offre più interfacce per l'aggiornamento dei dati nel data warehouse, tra cui, ad esempio, le istruzioni Data Manipulation Language (DML) nei job basati su SQL. Tuttavia, l'opzione migliore per le scritture di volumi elevati è l'API BigQuery Storage Write (gRPC).
      • Aggiunta anziché aggiornamento: anche se questa soluzione aggiungerà solo righe, non le aggiornerà, puoi sempre derivare le tabelle "stato attuale" in fase di query da un log di sola aggiunta, simulando così gli aggiornamenti.
  • Servizi di supporto
    • Secret Manager: Secret Manager contiene i valori dei secret per assicurarsi che non vengano archiviati in posizioni eccessivamente accessibili, ad esempio direttamente nella configurazione della funzione.
    • Identity and Access Management (IAM): IAM autorizza la funzione ad accedere al secret necessario in fase di runtime e a scrivere nella tabella BigQuery prevista.
    • Cloud Build: sebbene Cloud Build non venga trattato in profondità in questa pagina, Cloud Run Functions lo utilizza in background e puoi utilizzare Cloud Build per automatizzare gli aggiornamenti di deployment continuo delle funzioni dalle modifiche al codice sorgente in un repository Git.
  • Autenticazione di azioni e utenti
    • Service account Cloud Run Il modo principale e più semplice per utilizzare le azioni di Looker per l'integrazione con le risorse e gli asset proprietari della tua organizzazione è autenticare le richieste come provenienti dall'istanza di Looker utilizzando il meccanismo di autenticazione basato su token dell'API Looker Action e quindi autorizzare la funzione ad aggiornare i dati in BigQuery utilizzando un service account.
    • OAuth: un'altra opzione, non trattata in questa pagina, è l'utilizzo della funzionalità OAuth dell'API Looker Action. Questo approccio è più complesso e in genere non è necessario, ma può essere utilizzato se devi definire l'accesso degli utenti finali alla scrittura nella tabella utilizzando IAM, anziché il loro accesso in Looker o la logica ad hoc all'interno del codice della funzione.

Procedura dettagliata del codice demo

Abbiamo un singolo file contenente l'intera logica dell'azione demo disponibile su GitHub. In questa sezione, esamineremo gli elementi chiave del codice.

Codice di configurazione

La prima sezione contiene alcune costanti demo che identificano la tabella in cui l'azione scriverà. Nella sezione Guida al deployment più avanti in questa pagina, ti verrà chiesto di sostituire l'ID progetto con il tuo, che sarà l'unica modifica necessaria al codice.

/*** Demo constants */
const projectId = "your-project-id"
const datasetId = "demo_dataset"
const tableId = "demo_table"

La sezione successiva dichiara e inizializza alcune dipendenze del codice che l'azione utilizzerà. Forniamo un esempio che accede a Secret Manager "nel codice" utilizzando il modulo Node.js di Secret Manager; tuttavia, puoi anche eliminare questa dipendenza del codice utilizzando la funzionalità integrata di Cloud Run Functions per recuperare un secret durante l'inizializzazione.

/*** Code Dependencies ***/
const crypto = require("crypto")
const {SecretManagerServiceClient} = require('@google-cloud/secret-manager')
const secrets = new SecretManagerServiceClient()
const BigqueryStorage = require('@google-cloud/bigquery-storage')
const BQSManagedWriter = BigqueryStorage.managedwriter

Tieni presente che le dipendenze @google-cloud a cui viene fatto riferimento sono dichiarate anche nel file package.json per consentire il precaricamento e la disponibilità delle dipendenze nel runtime Node.js. crypto è un modulo Node.js integrato e non è dichiarato in package.json.

Gestione e routing delle richieste HTTP

L'interfaccia principale che il codice espone al runtime di Cloud Run Functions è una funzione JavaScript esportata che segue le convenzioni del server web Node.js Express. In particolare, la funzione riceve due argomenti: il primo rappresenta la richiesta HTTP, da cui puoi leggere vari parametri e valori della richiesta; il secondo rappresenta un oggetto di risposta, a cui invii i dati di risposta. Sebbene il nome della funzione possa essere qualsiasi, dovrai fornirlo a Cloud Run Functions in un secondo momento, come descritto nella sezione Guida al deployment.

/*** Entry-point for requests ***/
exports.httpHandler = async function httpHandler(req,res) {

La prima sezione della funzione httpHandler dichiara le varie route che la nostra azione riconoscerà, rispecchiando da vicino gli endpoint richiesti dell'API Action per una singola azione e le funzioni che gestiranno ogni route, definite più avanti nel file.

Sebbene alcuni esempi di azioni + Cloud Run Functions eseguano il deployment di una funzione separata per ogni route di questo tipo per allinearsi uno a uno con il routing predefinito di Cloud Run Functions, le funzioni sono in grado di applicare un "sub-routing" aggiuntivo all'interno del codice, come illustrato qui. In definitiva, è una questione di preferenza, ma l'esecuzione di questo routing aggiuntivo nel codice riduce al minimo il numero di funzioni di cui dobbiamo eseguire il deployment e ci aiuta a mantenere un singolo stato di codice coerente in tutti gli endpoint delle azioni.

    const routes = {
        "/": [hubListing],
        "/status": [hubStatus], // Debugging endpoint. Not required.
        "/action-0/form": [
            requireInstanceAuth,
            action0Form
            ],
        "/action-0/execute": [
            requireInstanceAuth,
            processRequestBody,
            action0Execute
            ]
        }

Il resto della funzione del gestore HTTP implementa la gestione della richiesta HTTP rispetto alle dichiarazioni di route precedenti e collega i valori restituiti da questi gestori all'oggetto di risposta.

    try {
        const routeHandlerSequence = routes[req.path] || [routeNotFound]
        for(let handler of routeHandlerSequence) {
            let handlerResponse = await handler(req)
            if (!handlerResponse) continue
            return res
                .status(handlerResponse.status || 200)
                .json(handlerResponse.body || handlerResponse)
            }
        }
    catch(err) {
        console.error(err)
        res.status(500).json("Unhandled error. See logs for details.")
        }
    }

Una volta completate le dichiarazioni di route e del gestore HTTP, esamineremo i tre endpoint di azione principali che dobbiamo implementare:

Endpoint dell'elenco delle azioni

Quando un amministratore di Looker collega per la prima volta un'istanza di Looker a un server di azioni, Looker chiama l'URL fornito, denominato "endpoint dell'elenco delle azioni", per ottenere informazioni sulle azioni disponibili tramite il server.

Nelle dichiarazioni di route che abbiamo mostrato in precedenza, abbiamo reso disponibile questo endpoint nel percorso principale (/) sotto l'URL della nostra funzione e abbiamo indicato che sarebbe stato gestito dalla funzione hubListing.

Come puoi vedere dalla seguente definizione di funzione, non c'è molto "codice": restituisce sempre gli stessi dati JSON. Una cosa da notare è che include dinamicamente il proprio URL in alcuni campi, consentendo all'istanza di Looker di inviare richieste successive alla stessa funzione.

async function hubListing(req){
    return {
        integrations: [
            {
                name: "demo-bq-insert",
                label: "Demo BigQuery Insert",
                supported_action_types: ["cell", "query", "dashboard"],
                form_url:`${process.env.CALLBACK_URL_PREFIX}/action-0/form`,
                url: `${process.env.CALLBACK_URL_PREFIX}/action-0/execute`,
                icon_data_uri: "data:image/png;base64,...",
                supported_formats:["inline_json"],
                supported_formattings:["unformatted"],
                required_fields:[
                    // You can use this to make your action available
                    // for specific queries/fields
                    // {tag:"user_id"}
                    ],
                params: [
                    // You can use this to require parameters, either
                    // from the Action's administrative configuration,
                    // or from the invoking user's user attributes.
                    // A common use case might be to have the Looker
                    // instance pass along the user's identification to
                    // allow you to conditionally authorize the action:
                    {name: "email", label: "Email", user_attribute_name: "email", required: true}
                    ]
                }
            ]
        }
    }

Per scopi dimostrativi, il nostro codice non ha richiesto l'autenticazione per recuperare questo elenco. Tuttavia, se ritieni che i metadati dell'azione siano sensibili, puoi anche richiedere l'autenticazione per questa route, come mostrato nella sezione successiva.

Tieni presente inoltre che la nostra funzione Cloud Run potrebbe esporre e gestire più azioni, il che spiega la nostra convenzione di route /action-X/.... Tuttavia, la nostra funzione Cloud Run demo implementerà una sola azione.

Endpoint del modulo di azione

Sebbene non tutti i casi d'uso richiedano un modulo, averne uno si adatta bene al caso d'uso dei writeback del database, in quanto gli utenti possono ispezionare i dati in Looker e poi fornire i valori da inserire nel database. Poiché il nostro elenco di azioni ha fornito un parametro form_url, Looker chiamerà questo endpoint del modulo di azione quando un utente inizia a interagire con l'azione, per determinare quali dati aggiuntivi acquisire dall'utente.

Nelle dichiarazioni di route, abbiamo reso disponibile questo endpoint nel percorso /action-0/form e gli abbiamo associato due gestori: requireInstanceAuth e action0Form.

Abbiamo configurato le dichiarazioni di route per consentire più gestori di questo tipo perché alcune logiche possono essere riutilizzate per più endpoint.

Ad esempio, possiamo vedere che requireInstanceAuth viene utilizzato per più route. Utilizziamo questo gestore ovunque vogliamo richiedere che una richiesta provenga dalla nostra istanza di Looker. Il gestore recupera il valore del token secret previsto da Secret Manager e rifiuta tutte le richieste che non hanno il valore del token previsto.

async function requireInstanceAuth(req) {
    const lookerSecret = await getLookerSecret()
    if(!lookerSecret){return}
    const expectedAuthHeader = `Token token="${lookerSecret}"`
    if(!timingSafeEqual(req.headers.authorization,expectedAuthHeader)){
        return {
            status:401,
            body: {error: "Looker instance authentication is required"}
            }
        }
    return

    function timingSafeEqual(a, b) {
        if(typeof a !== "string"){return}
        if(typeof b !== "string"){return}
        var aLen = Buffer.byteLength(a)
        var bLen = Buffer.byteLength(b)
        const bufA = Buffer.allocUnsafe(aLen)
        bufA.write(a)
        const bufB = Buffer.allocUnsafe(aLen) //Yes, aLen
        bufB.write(b)

        return crypto.timingSafeEqual(bufA, bufB) && aLen === bLen;
        }
    }

Tieni presente che utilizziamo un'implementazione timingSafeEqual, anziché il controllo di uguaglianza standard (==), per evitare di divulgare informazioni di temporizzazione del canale laterale che consentirebbero a un utente malintenzionato di scoprire rapidamente il valore del nostro secret.

Supponendo che una richiesta superi il controllo di autenticazione dell'istanza, viene gestita dal gestore action0Form.

async function action0Form(req){
    return [
        {name: "choice",  label: "Choose", type:"select", options:[
            {name:"Yes", label:"Yes"},
            {name:"No", label:"No"},
            {name:"Maybe", label:"Maybe"}
            ]},
        {name: "note", label: "Note", type: "textarea"}
        ]
    }

Sebbene il nostro esempio demo sia molto statico, il codice del modulo può essere più interattivo per determinati casi d'uso. Ad esempio, a seconda della selezione di un utente in un elenco a discesa iniziale, è possibile visualizzare campi diversi.

Endpoint di esecuzione dell'azione

L'endpoint di esecuzione dell'azione è il punto in cui risiede la maggior parte della logica di qualsiasi azione e in cui esamineremo la logica specifica per il caso d'uso dell'inserimento di BigQuery.

Nelle dichiarazioni di route, abbiamo reso disponibile questo endpoint nel percorso /action-0/execute e gli abbiamo associato tre gestori: requireInstanceAuth, processRequestBody e action0Execute.

Abbiamo già trattato requireInstanceAuth e il gestore processRequestBody fornisce principalmente una pre-elaborazione non interessante per convertire alcuni campi scomodi nel corpo della richiesta di Looker in un formato più pratico, ma puoi farvi riferimento nel file di codice completo.

La funzione action0Execute inizia mostrando esempi di estrazione di informazioni da diverse parti della richiesta di azione che potrebbero essere utili. In pratica, tieni presente che gli elementi della richiesta a cui il nostro codice fa riferimento come formParams e actionParams possono contenere campi diversi, a seconda di ciò che dichiari negli endpoint dell'elenco e del modulo.

async function action0Execute (req){
    try{
        // Prepare some data that we will insert
        const scheduledPlanId = req.body.scheduled_plan && req.body.scheduled_plan.scheduled_plan_id
        const formParams = req.body.form_params || {}
        const actionParams = req.body.data || {}
        const queryData = req.body.attachment.data //If using a standard "push" action

        /*In case any fields require datatype-specific preparation, check this example:
        https://github.com/googleapis/nodejs-bigquery-storage/blob/main/samples/append_rows_proto2.js
        */

        const newRow = {
            invoked_at: new Date(),
            invoked_by: actionParams.email,
            scheduled_plan_id: scheduledPlanId || null,
            query_result_size: queryData.length,
            choice: formParams.choice,
            note: formParams.note,
            }

Il codice passa quindi ad alcuni codici BigQuery standard per inserire effettivamente i dati. Tieni presente che le API BigQuery Storage Write offrono altre varianti più complesse, più adatte a una connessione di streaming persistente o a inserimenti collettivi di molti record; tuttavia, per rispondere alle singole interazioni degli utenti nel contesto di una funzione Cloud Run, questa è la variante più diretta.

await bigqueryConnectAndAppend(newRow)

...

async function bigqueryConnectAndAppend(row){
    let writerClient
    try{
        const destinationTablePath = `projects/${projectId}/datasets/${datasetId}/tables/${tableId}`
        const streamId = `${destinationTablePath}/streams/_default`
        writerClient = new BQSManagedWriter.WriterClient({projectId})
        const writeMetadata = await writerClient.getWriteStream({
            streamId,
            view: 'FULL',
            })
        const protoDescriptor = BigqueryStorage.adapt.convertStorageSchemaToProto2Descriptor(
            writeMetadata.tableSchema,
            'root'
            )
        const connection = await writerClient.createStreamConnection({
            streamId,
            destinationTablePath,
            })
        const writer = new BQSManagedWriter.JSONWriter({
            streamId,
            connection,
            protoDescriptor,
            })

        let result
        if(row){
            // The API expects an array of rows, so wrap the single row in an array
            const rowsToAppend = [row]
            result = await writer.appendRows(rowsToAppend).getResult()
            }
        return {
            streamId: connection.getStreamId(),
            protoDescriptor,
            result
            }
        }
    catch (e) {throw e}
    finally{
        if(writerClient){writerClient.close()}
        }
    }

Il codice demo include anche un endpoint "stato" per la risoluzione dei problemi, ma questo endpoint non è obbligatorio per l'integrazione dell'API Action.

Guida al deployment

Infine, forniremo una guida passo passo per eseguire il deployment della demo, che include prerequisiti, deployment di Cloud Run Functions, configurazione di BigQuery e configurazione di Looker.

Prerequisiti di progetto e servizio

Prima di iniziare a configurare dettagli specifici, esamina questo elenco per comprendere i servizi e le policy di cui la soluzione avrà bisogno:

  1. Un nuovo progetto: avrai bisogno di un nuovo progetto per ospitare le risorse del nostro esempio.
  2. Servizi: quando utilizzi per la prima volta BigQuery e Cloud Run Functions nell'interfaccia utente della console Cloud, ti verrà chiesto di abilitare le API richieste per i servizi necessari, tra cui BigQuery, Artifact Registry, Cloud Build, Cloud Functions, Cloud Logging, Pub/Sub, Cloud Run Admin e Secret Manager.
  3. Policy per le chiamate non autenticate: questo caso d'uso richiede di eseguire il deployment di Cloud Run Functions che "consentono l'accesso pubblico", poiché gestiremo l'autenticazione per le richieste in entrata nel nostro codice in base all'API Action, anziché utilizzare IAM. Sebbene questa operazione sia consentita per impostazione predefinita, la policy dell'organizzazione spesso ne limita l'utilizzo. In particolare, la policy constraints/iam.allowedPolicyMemberDomains limita a chi possono essere concesse le autorizzazioni IAM e potrebbe essere necessario modificarla per consentire l'entità allUsers per l'accesso non autenticato. Per ulteriori informazioni, consulta questa guida, su come creare servizi Cloud Run pubblici quando è applicata la condivisione con limitazioni di dominio se non riesci a consentire l'accesso pubblico.
  4. Altre policy: tieni presente che anche altri Google Cloud vincoli delle policy dell'organizzazione possono impedire il deployment di servizi altrimenti consentiti per impostazione predefinita.

Eseguire il deployment di Cloud Run Functions

Dopo aver creato un nuovo progetto, segui questi passaggi per eseguire il deployment di Cloud Run Functions

  1. In Cloud Run Functions, fai clic su Crea funzione.
  2. Scegli un nome per la funzione (ad esempio, "demo-bq-insert-action").
  3. Nelle impostazioni Trigger :
    1. Il tipo di trigger dovrebbe essere già "HTTPS".
    2. Imposta Autenticazione su Consenti chiamate non autenticate.
    3. Copia il valore URL negli appunti.
  4. Nelle impostazioni Runtime > Variabili di ambiente runtime :
    1. Fai clic su Aggiungi variabile.
    2. Imposta il nome della variabile su CALLBACK_URL_PREFIX.
    3. Incolla l'URL del passaggio precedente come valore.
  5. Fai clic su Avanti.
  6. Fai clic sul file package.json e incolla i contenuti.
  7. Fai clic sul file index.js e incolla i contenuti.
  8. Assegna la variabile projectId nella parte superiore del file al tuo ID progetto.
  9. Imposta Entry Point su httpHandler.
  10. Fai clic su Esegui il deployment.
  11. Concedi le autorizzazioni richieste (se presenti) al service account di build.
  12. Attendi il completamento del deployment.
  13. Se, in uno dei passaggi futuri, ricevi un errore che ti chiede di esaminare i Google Cloud log, tieni presente che puoi accedere ai log di questa funzione dalla scheda Log in questa pagina.
  14. Prima di uscire dalla pagina di Cloud Run Functions, nella scheda Dettagli, individua e annota il service account della funzione. Lo utilizzeremo nei passaggi successivi per assicurarci che la funzione disponga delle autorizzazioni necessarie.
  15. Testa il deployment della funzione direttamente nel browser visitando l'URL. Dovresti visualizzare una risposta JSON contenente l'elenco delle integrazioni.
  16. Se ricevi un errore 403, il tentativo di impostare Consenti chiamate non autenticate potrebbe non essere riuscito in modo silenzioso a causa di una policy dell'organizzazione. Controlla se la funzione consente le chiamate non autenticate, esamina l'impostazione della policy dell'organizzazione e prova ad aggiornarla.

Accesso alla tabella di destinazione BigQuery

In pratica, la tabella di destinazione in cui inserire i dati può risiedere in un progetto diverso Google Cloud ; tuttavia, a scopo dimostrativo, creeremo una nuova tabella di destinazione nello stesso progetto. In entrambi i casi, devi assicurarti che il service account di Cloud Run Functions disponga delle autorizzazioni per scrivere nella tabella.

  1. Accedi alla console BigQuery.
  2. Crea la tabella demo:

    1. Nella barra Spazio di esplorazione, utilizza il menu con i puntini di sospensione accanto al progetto e seleziona Crea set di dati.
    2. Assegna l'ID demo_dataset al set di dati e fai clic su Crea set di dati.
    3. Utilizza il menu con i puntini di sospensione nel set di dati appena creato e seleziona Crea tabella.
    4. Assegna il nome demo_table alla tabella.
    5. In Schema, seleziona Modifica come testo, utilizza lo schema seguente e fai clic su Crea tabella.

      [
       {"name":"invoked_at","type":"TIMESTAMP"},
       {"name":"invoked_by","type":"STRING"},
       {"name":"scheduled_plan_id","type":"STRING"},
       {"name":"query_result_size","type":"INTEGER"},
       {"name":"choice","type":"STRING"},
       {"name":"note","type":"STRING"}
      ]
      
  3. Assegna le autorizzazioni:

    1. Nella barra Spazio di esplorazione, fai clic sul set di dati.
    2. Nella pagina set di dati, fai clic su Condivisione > Autorizzazioni.
    3. Fai clic su Aggiungi entità.
    4. Imposta la nuova entità sul service account della funzione, annotato in precedenza in questa pagina.
    5. Assegna il ruolo BigQuery Data Editor.
    6. Fai clic su Salva.

Connettersi a Looker

Ora che la funzione è stata sottoposta a deployment, collegheremo Looker.

  1. Avremo bisogno di un secret condiviso per l'azione per autenticare che le richieste provengano dall'istanza di Looker. Genera una stringa casuale lunga e mantienila al sicuro. La utilizzeremo nei passaggi successivi come valore del secret di Looker.
  2. Nella console Cloud, vai a Secret Manager.
    1. Fai clic su Crea secret.
    2. Imposta il nome su LOOKER_SECRET. (Questo è hardcoded nel codice per questa demo, ma puoi scegliere qualsiasi nome quando lavori con il tuo codice.)
    3. Imposta il valore del secret sul valore del secret che hai generato.
    4. Fai clic su Crea secret.
    5. Nella pagina Secret, fai clic sulla scheda Autorizzazioni.
    6. Fai clic su Concedi l'accesso.
    7. Imposta Nuove entità sul service account della funzione, annotato in precedenza.
    8. Assegna il ruolo Secret Manager Secret Accessor.
    9. Fai clic su Salva.
    10. Puoi verificare che la funzione acceda correttamente al secret visitando la route /status aggiunta all'URL della funzione.
  3. Nell'istanza di Looker:
    1. Vai ad Amministratore > Piattaforma > Azioni.
    2. Vai in fondo alla pagina per fare clic su Aggiungi hub azioni.
    3. Fornisci l'URL della funzione (ad esempio, https://your-region-your-project.cloudfunctions.net/demo-bq-insert-action) e conferma facendo clic su Aggiungi hub azioni.
    4. Ora dovresti vedere una nuova voce dell'hub azioni con un'azione denominata Demo BigQuery Insert.
    5. Nella voce dell'hub azioni, fai clic su Configura autorizzazione.
    6. Inserisci il secret di Looker generato nel campo Token di autorizzazione e fai clic su Aggiorna token.
    7. Nell'azione Demo BigQuery Insert, fai clic su Abilita.
    8. Attiva l'opzione Abilitato.
    9. Un test dell'azione dovrebbe essere eseguito automaticamente, confermando che la funzione accetta la richiesta di Looker e risponde correttamente all'endpoint del modulo.
    10. Fai clic su Salva.

Test end-to-end

Ora dovremmo essere in grado di utilizzare effettivamente la nostra nuova azione. Questa azione è configurata per funzionare con qualsiasi query, quindi scegli un'esplorazione (ad esempio, un'esplorazione di attività di sistema integrata), aggiungi alcuni campi a una nuova query, eseguila e poi scegli Invia dal menu a forma di ingranaggio. Dovresti vedere l'azione come una delle destinazioni disponibili e ti verrà chiesto di inserire alcuni campi:

Screenshot della finestra modale "Invia" di Looker con la nostra nuova azione selezionata

Dopo aver premuto Invia, dovresti avere una nuova riga inserita nella tabella BigQuery (e l'indirizzo email del tuo account utente Looker identificato nella colonna invoked_by)!