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 generazione di report sui dati nel data warehouse per scrivere nuovamente e aggiornare il data warehouse.

Tramite la propria API Action, Looker supporta questo caso d'uso per qualsiasi data warehouse o destinazione. Questa pagina di documentazione illustra ai clienti che utilizzano l' Google Cloud infrastruttura come eseguire il deployment di una soluzione sulle funzioni Cloud Run per scrivere nuovamente 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.

  • Funzioni Cloud Run
    • Perché scegliere Cloud Run Functions? In qualità di offerta "serverless " di Google, Cloud Run Functions è un'ottima scelta per la semplicità di gestione e manutenzione. Un aspetto 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.
    • Lingua e runtime Le funzioni Cloud Run supportano più lingue e runtime. Questa pagina di documentazione si concentra su un esempio in JavaScript e Node.js. Tuttavia, i concetti sono direttamente traducibili nelle altre lingue e nei runtime supportati.
  • BigQuery
    • Perché BigQuery? Sebbene questa pagina della documentazione preveda 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:BigQuery offre più interfacce per aggiornare i 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 ad alto volume è l'API BigQuery Storage Write.
      • Aggiunta anziché aggiornamento: anche se questa soluzione aggiunge solo righe, non le aggiorna, puoi sempre ricavare le tabelle "stato corrente" al momento della query da un log di sola aggiunta, simulando così gli aggiornamenti.
  • Servizi di supporto
    • Secret Manager: Secret Manager memorizza i valori dei secret per assicurarsi che non vengano archiviati in luoghi troppo accessibili, ad esempio direttamente nella configurazione della funzione.
    • Identity and Access Management (IAM): IAM autorizza la funzione ad accedere al segreto necessario in fase di esecuzione e a scrivere nella tabella BigQuery prevista.
    • Cloud Build: anche se Cloud Build non verrà trattato in dettaglio in questa pagina, le funzioni Cloud Run lo utilizzano in background e puoi utilizzarlo per automatizzare gli aggiornamenti delle funzioni di cui viene eseguito il deployment continuo a partire dalle modifiche al codice sorgente in un repository Git.
  • Azione e autenticazione utente
    • Account di servizio 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 dalla tua istanza di Looker utilizzando il meccanismo di autenticazione basato su token dell'API Looker Actions e poi autorizzare la funzione ad aggiornare i dati in BigQuery utilizzando un account di servizio.
    • OAuth:un'altra opzione, non trattata in questa pagina, è utilizzare la funzionalità OAuth dell'API Looker Action. Questo approccio è più complesso e generalmente non necessario, ma può essere utilizzato se devi definire l'accesso degli utenti finali alla scrittura nella tabella utilizzando IAM, anziché utilizzare il loro accesso in Looker o una logica ad hoc all'interno del codice della funzione.

Procedura dettagliata del codice demo

Abbiamo un unico 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 di dimostrazione che identificano la tabella in cui l'azione scriverà. Nella sezione Guida all'implementazione di 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 di codice che verranno utilizzate dall'azione. Forniamo un esempio che accede a Secret Manager "in-code" utilizzando il modulo Node.js di Secret Manager. Tuttavia, puoi anche eliminare questa dipendenza dal codice utilizzando la funzionalità integrata di Cloud Run per recuperare un segreto 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 si fa riferimento sono dichiarate anche nel nostro file package.json per consentire il pre-caricamento e la disponibilità delle dipendenze per il nostro runtime Node.js. crypto è un modulo Node.js integrato e non è dichiarato in package.json.

Gestione e routing delle richieste HTTP

L'interfaccia principale esposta dal codice al runtime delle funzioni Cloud Run è 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 di richiesta; il secondo rappresenta un oggetto di risposta a cui emetti i dati di risposta. Sebbene il nome della funzione possa essere qualsiasi, dovrai fornirlo alle funzioni Cloud Run 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 i vari percorsi che la nostra azione riconoscerà, rispecchiando da vicino gli endpoint richiesti dall'API Action per una singola azione e le funzioni che gestiranno ogni percorso, definite più avanti nel file.

Sebbene alcuni esempi di azioni e funzioni Cloud Run implementino una funzione separata per ogni percorso per allinearsi uno a uno con il routing predefinito delle funzioni Cloud Run, le funzioni sono in grado di applicare un "routing secondario " aggiuntivo all'interno del codice, come dimostrato qui. In definitiva, si tratta di una questione di preferenza, ma questo routing aggiuntivo in-code riduce al minimo il numero di funzioni da implementare 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 in base 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 eliminate le dichiarazioni del gestore HTTP e delle route, esamineremo i tre endpoint di azioni principali che dobbiamo implementare:

Endpoint dell'elenco di 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 di azioni", per ottenere informazioni sulle azioni disponibili tramite il server.

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

Come puoi vedere dalla definizione di funzione seguente, non c'è troppo "codice": restituisce sempre gli stessi dati JSON. 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 questa scheda. Tuttavia, se ritieni che i metadati delle azioni siano sensibili, puoi richiedere l'autenticazione anche per questo percorso, come mostrato nella sezione successiva.

Tieni inoltre presente 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 di demo Cloud Run implementerà una sola azione.

Endpoint modulo di azioni

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

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

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

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

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 di timingSafeEqual, anziché il controllo di uguaglianza standard (==), per evitare la fuga di informazioni sui tempi dei canali laterali che potrebbero consentire a un malintenzionato di capire rapidamente il valore del nostro segreto.

Se una richiesta supera il controllo di autenticazione dell'istanza, viene gestita dall'handler 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"}
        ]
    }

Anche se il nostro esempio di demo è 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 menu a discesa iniziale, possono essere visualizzati campi diversi.

Endpoint di esecuzione dell'azione

L'endpoint di esecuzione dell'azione è dove risiede la maggior parte della logica di qualsiasi azione e dove entreremo nella logica specifica per il caso d'uso di inserimento di BigQuery.

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

Abbiamo già trattato requireInstanceAuth e l'handler processRequestBody fornisce una preelaborazione per lo più non interessante per trasformare determinati 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 di richiesta a cui il nostro codice fa riferimento come formParams e actionParams possono contenere campi diversi, a seconda di ciò che dichiari negli endpoint di scheda e 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 a un codice BigQuery standard per inserire effettivamente i dati. Tieni presente che le API BigQuery Storage Write offrono altre varianti più complesse che sono più adatte per una connessione di streaming persistente o per 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 di esempio include anche un endpoint "status" per la risoluzione dei problemi, ma questo endpoint non è necessario per l'integrazione dell'API Action.

Guida al deployment

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

Prerequisiti di progetti e servizi

Prima di iniziare a configurare le specifiche, esamina questo elenco per capire quali servizi e criteri saranno necessari per la soluzione:

  1. Un nuovo progetto:avrai bisogno di un nuovo progetto per ospitare le risorse del nostro esempio.
  2. Servizi:la prima volta che utilizzi le funzioni BigQuery e Cloud Run nell'interfaccia utente della console Cloud, ti verrà chiesto di attivare 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. Criterio per le chiamate non autenticate: questo caso d'uso richiede il deployment di funzioni Cloud Run che "consentono chiamate non autenticate", poiché gestiremo l'autenticazione per le richieste in arrivo nel nostro codice in base all'API Action, anziché utilizzare IAM. Sebbene sia consentito per impostazione predefinita, i criteri dell'organizzazione spesso limitano questo utilizzo. Nello specifico, il criterio constraints/iam.allowedPolicyMemberDomains limita chi può ricevere autorizzazioni IAM e potrebbe essere necessario modificarlo per consentire all'entità allUsers l'accesso non autenticato. Per ulteriori informazioni, consulta questa guida Come creare servizi Cloud Run pubblici quando è impostata la condivisione con restrizioni al dominio se non riesci ad autorizzare le chiamate non autenticate.
  4. Altri criteri:tieni presente che anche altri Google Cloud vincoli dei criteri dell'organizzazione possono impedire il deployment di servizi altrimenti consentiti per impostazione predefinita.

Esegui il deployment della funzione Cloud Run

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

  1. In Funzioni Cloud Run, 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 attivatore 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 Punto di ingresso su httpHandler.
  10. Fai clic su Esegui il deployment.
  11. Concedi le autorizzazioni richieste (se presenti) all'account di servizio di compilazione.
  12. Attendi il completamento del deployment.
  13. Se in un secondo momento ricevi un messaggio di 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 della funzione Cloud Run, nella scheda Dettagli, individua e prendi nota dell'account di servizio 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 la scheda dell'integrazione.
  16. Se ricevi un errore 403, il tentativo di impostare Consenti chiamate non autenticate potrebbe non essere andato a buon fine in modo silenzioso a causa di un criterio dell'organizzazione. Controlla se la funzione consente chiamate non autenticate, esamina l'impostazione delle norme dell'organizzazione e prova ad aggiornarla.

Accesso alla tabella di destinazione BigQuery

In pratica, la tabella di destinazione in cui inserire i dati può trovarsi in un progetto Google Cloud diverso, ma, a scopo dimostrativo, creeremo una nuova tabella di destinazione nello stesso progetto. In entrambi i casi, devi assicurarti che l'account di servizio della funzione Cloud Run disponga delle autorizzazioni per scrivere nella tabella.

  1. Vai alla console BigQuery.
  2. Crea la tabella di esempio:

    1. Nella barra di esplorazione, utilizza il menu con i tre puntini accanto al progetto e seleziona Crea set di dati.
    2. Assegna al set di dati l'ID demo_dataset e fai clic su Crea set di dati.
    3. Utilizza il menu con i tre puntini sul set di dati appena creato e seleziona Crea tabella.
    4. Assegna alla tabella il nome demo_table.
    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 Explorer, fai clic sul set di dati.
    2. Nella pagina del set di dati, fai clic su Condivisione > Autorizzazioni.
    3. Fai clic su Aggiungi entità.
    4. Imposta Nuova entità sull'account di servizio per la funzione, indicato in precedenza in questa pagina.
    5. Assegna il ruolo Editor dati BigQuery.
    6. Fai clic su Salva.

Connessione a Looker

Ora che la funzione è stata implementata, collegheremo Looker.

  1. Per la tua azione avremo bisogno di un segreto condiviso per autenticare le richieste provenienti dalla tua istanza Looker. Genera una stringa casuale lunga e tienila al sicuro. Lo utilizzeremo nei passaggi successivi come valore del token segreto di Looker.
  2. Nella console Cloud, vai a Secret Manager.
    1. Fai clic su Crea secret.
    2. Imposta Nome su LOOKER_SECRET. Questo valore è hardcoded nel codice di questa demo, ma puoi scegliere qualsiasi nome quando utilizzi il tuo codice.
    3. Imposta Valore segreto sul valore segreto 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à sull'account di servizio per la tua funzione, indicato in precedenza.
    8. Assegna il ruolo Funzione di accesso ai secret di Secret Manager.
    9. Fai clic su Salva.
    10. Puoi verificare che la funzione acceda correttamente al segreto visitando il percorso /status aggiunto all'URL della funzione.
  3. Nell'istanza di Looker:
    1. Vai ad Amministrazione > Piattaforma > Azioni.
    2. Vai in fondo alla pagina e fai clic su Aggiungi Hub di 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 Action Hub.
    4. Ora dovresti vedere una nuova voce di Action Hub con un'azione denominata Demo BigQuery Insert.
    5. Nella voce Hub di azioni, fai clic su Configura autorizzazione.
    6. Inserisci il token segreto di Looker generato nel campo Token di autorizzazione e fai clic su Aggiorna token.
    7. Nell'azione Demo BigQuery Insert (Inserisci BigQuery di prova), fai clic su Attiva.
    8. Attiva l'opzione Attivata.
    9. Dovrebbe essere eseguito automaticamente un test dell'azione, che confermi 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 la nuova azione. Questa azione è configurata per funzionare con qualsiasi query, quindi scegli un'esplorazione (ad esempio un'esplorazione dell'attività di sistema integrata), aggiungi alcuni campi a una nuova query, eseguila e 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 nuova azione selezionata

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