Écritures BigQuery à l'aide d'actions Looker sur les fonctions Cloud Run

De nombreux clients Looker souhaitent permettre à leurs utilisateurs de ne pas se contenter de créer des rapports sur les données de leur entrepôt de données, mais aussi de les modifier.

Grâce à son API Action, Looker prend en charge ce cas d'utilisation pour n'importe quel entrepôt de données ou destination. Cette page de documentation explique aux clients qui utilisent l'infrastructure Google Cloud comment déployer une solution sur des fonctions Cloud Run pour écrire à nouveau dans BigQuery. Cette page aborde les sujets suivants :

Considérations concernant la solution

Utilisez cette liste de considérations pour vérifier que cette solution répond à vos besoins.

  • Fonctions Cloud Run
    • Pourquoi utiliser les fonctions Cloud Run ? En tant qu'offre "sans serveur" de Google, les fonctions Cloud Run sont un excellent choix pour faciliter les opérations et la maintenance. Gardez à l'esprit que la latence, en particulier pour les invocations à froid, peut être plus longue qu'avec une solution reposant sur un serveur dédié.
    • Langage et environnement d'exécution Les fonctions Cloud Run sont compatibles avec plusieurs langages et environnements d'exécution. Cette page de documentation présente un exemple en JavaScript et Node.js. Toutefois, les concepts sont directement transposables dans les autres langues et environnements d'exécution compatibles.
  • BigQuery
    • Pourquoi BigQuery ? Bien que cette page de documentation suppose que vous utilisiez déjà BigQuery, il s'agit d'un excellent choix pour un entrepôt de données en général. Tenez compte des points suivants :
      • API BigQuery Storage Write:BigQuery propose plusieurs interfaces pour mettre à jour les données de votre entrepôt de données, y compris, par exemple, des instructions de langage de manipulation de données (DML) dans des tâches basées sur SQL. Toutefois, l'option la plus adaptée pour les écritures à fort volume est l'API BigQuery Storage Write.
      • Ajout plutôt que mise à jour:même si cette solution ne permet que d'ajouter des lignes, et non de les mettre à jour, vous pouvez toujours dériver des tables "état actuel" au moment de la requête à partir d'un journal en mode ajout uniquement, ce qui permet de simuler les mises à jour.
  • Services d'assistance
    • Secret Manager:Secret Manager contient des valeurs secrètes pour s'assurer qu'elles ne sont pas stockées dans des emplacements trop accessibles, comme directement dans la configuration de la fonction.
    • Identity and Access Management (IAM) (Gestion de l'identité et des accès):IAM autorise la fonction à accéder au secret nécessaire au moment de l'exécution et à écrire dans la table BigQuery prévue.
    • Cloud Build:bien que Cloud Build ne soit pas abordé en détail sur cette page, les fonctions Cloud Run l'utilisent en arrière-plan. Vous pouvez utiliser Cloud Build pour automatiser les mises à jour continues de vos fonctions à partir de modifications apportées à votre code source dans un dépôt Git.
  • Action et authentification des utilisateurs
    • Compte de service Cloud Run Le moyen le plus simple et le plus efficace d'utiliser les actions Looker pour intégrer les ressources et composants first party de votre organisation consiste à authentifier les requêtes comme provenant de votre instance Looker à l'aide du mécanisme d'authentification basé sur les jetons de l'API Looker Action, puis à autoriser la fonction à mettre à jour les données dans BigQuery à l'aide d'un compte de service.
    • OAuth:une autre option, qui n'est pas abordée sur cette page, consiste à utiliser la fonctionnalité OAuth de l'API Looker Action. Cette approche est plus complexe et n'est généralement pas nécessaire, mais elle peut être utilisée si vous devez définir l'accès des utilisateurs finaux à l'écriture dans le tableau à l'aide d'IAM, plutôt que d'utiliser leur accès dans Looker ou une logique ad hoc dans le code de votre fonction.

Tutoriel du code de démonstration

Un seul fichier contenant l'intégralité de la logique de notre action de démonstration est disponible sur GitHub. Dans cette section, nous allons passer en revue les principaux éléments du code.

Code de configuration

La première section contient quelques constantes de démonstration qui identifient la table dans laquelle l'action écrira. Dans la section Guide de déploiement plus loin sur cette page, vous serez invité à remplacer l'ID de projet par le vôtre. Il s'agit de la seule modification nécessaire au code.

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

La section suivante déclare et initialise quelques dépendances de code que votre action utilisera. Nous fournissons un exemple qui accède à Secret Manager "dans le code" à l'aide du module Node.js de Secret Manager. Toutefois, vous pouvez également éliminer cette dépendance de code en utilisant la fonctionnalité intégrée des fonctions Cloud Run pour récupérer un secret à votre place lors de son initialisation.

/*** 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

Notez que les dépendances @google-cloud référencées sont également déclarées dans notre fichier package.json pour permettre leur préchargement et leur disponibilité pour notre environnement d'exécution Node.js. crypto est un module Node.js intégré et n'est pas déclaré dans package.json.

Gestion et routage des requêtes HTTP

L'interface principale que votre code expose à l'environnement d'exécution des fonctions Cloud Run est une fonction JavaScript exportée qui suit les conventions du serveur Web Express Node.js. Plus précisément, votre fonction reçoit deux arguments: le premier représente la requête HTTP, à partir de laquelle vous pouvez lire divers paramètres et valeurs de requête ; et le second représente un objet de réponse, auquel vous envoyez vos données de réponse. Bien que vous puissiez attribuer le nom de votre choix à la fonction, vous devrez le fournir aux fonctions Cloud Run plus tard, comme indiqué dans la section Guide de déploiement.

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

La première section de la fonction httpHandler déclare les différents parcours que notre action reconnaîtra, en reproduisant fidèlement les points de terminaison requis par l'API Action pour une seule action, ainsi que les fonctions qui géreront chaque parcours, définies plus loin dans le fichier.

Bien que certains exemples d'actions et de fonctions Cloud Run déploient une fonction distincte pour chaque route de ce type afin de s'aligner individuellement sur le routage par défaut des fonctions Cloud Run, les fonctions peuvent appliquer un "sous-routage" supplémentaire dans leur code, comme illustré ici. Il s'agit en fin de compte d'une question de préférence, mais effectuer ce routage supplémentaire dans le code réduit le nombre de fonctions que nous devons déployer et nous aide à maintenir un état de code cohérent unique pour tous les points de terminaison des actions.

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

Le reste de la fonction de gestionnaire HTTP implémente le traitement de la requête HTTP par rapport aux déclarations de route précédentes et associe les valeurs renvoyées par ces gestionnaires à l'objet de réponse.

    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.")
        }
    }

Maintenant que le gestionnaire HTTP et les déclarations de route sont en place, nous allons nous pencher sur les trois principaux points de terminaison d'action que nous devons implémenter:

Point de terminaison de la liste des actions

Lorsqu'un administrateur Looker connecte une instance Looker à un serveur d'actions pour la première fois, Looker appelle l'URL fournie, appelée point de terminaison de la liste des actions, pour obtenir des informations sur les actions disponibles via le serveur.

Dans les déclarations de routes que nous avons présentées précédemment, nous avons rendu ce point de terminaison disponible au niveau du chemin d'accès racine (/) sous l'URL de notre fonction et indiqué qu'il serait géré par la fonction hubListing.

Comme vous pouvez le constater dans la définition de fonction suivante, elle ne contient pas beaucoup de "code". Elle renvoie simplement les mêmes données JSON à chaque fois. Notez qu'il inclut dynamiquement sa propre URL dans certains des champs, ce qui permet à l'instance Looker d'envoyer ultérieurement des requêtes à la même fonction.

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}
                    ]
                }
            ]
        }
    }

À des fins de démonstration, notre code n'a pas requis d'authentification pour récupérer cette fiche. Toutefois, si vous considérez que les métadonnées de vos actions sont sensibles, vous pouvez également exiger une authentification pour ce parcours, comme indiqué dans la section suivante.

Notez également que notre fonction Cloud Run peut exposer et gérer plusieurs actions, ce qui explique notre convention de routage /action-X/.... Toutefois, notre fonction Cloud Run de démonstration n'implémentera qu'une seule action.

Point de terminaison de formulaire d'action

Bien que tous les cas d'utilisation ne nécessitent pas de formulaire, un formulaire est adapté au cas d'utilisation des écritures dans la base de données, car les utilisateurs peuvent inspecter les données dans Looker, puis fournir des valeurs à insérer dans la base de données. Étant donné que notre liste d'actions a fourni un paramètre form_url, Looker appellera ce point de terminaison de formulaire d'action lorsqu'un utilisateur commencera à interagir avec votre action pour déterminer quelles données supplémentaires collecter auprès de l'utilisateur.

Dans nos déclarations de routes, nous avons rendu ce point de terminaison disponible sous le chemin d'accès /action-0/form et lui avons associé deux gestionnaires: requireInstanceAuth et action0Form.

Nous avons configuré nos déclarations de routes pour autoriser plusieurs gestionnaires de ce type, car certaines logiques peuvent être réutilisées pour plusieurs points de terminaison.

Par exemple, nous pouvons voir que requireInstanceAuth est utilisé pour plusieurs routes. Nous utilisons ce gestionnaire chaque fois que nous voulons exiger qu'une requête provienne de notre instance Looker. Le gestionnaire récupère la valeur du jeton attendue du secret auprès de Secret Manager et rejette toutes les requêtes qui ne comportent pas cette valeur de jeton attendue.

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;
        }
    }

Notez que nous utilisons une implémentation timingSafeEqual plutôt que la vérification d'égalité standard (==) pour éviter les fuites d'informations temporelles de canal auxiliaire qui permettraient à un pirate informatique de déterminer rapidement la valeur de notre secret.

En supposant qu'une requête passe la vérification d'authentification de l'instance, elle est ensuite gérée par le gestionnaire 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"}
        ]
    }

Bien que notre exemple de démonstration soit très statique, le code du formulaire peut être plus interactif pour certains cas d'utilisation. Par exemple, en fonction de la sélection d'un utilisateur dans un menu déroulant initial, différents champs peuvent s'afficher.

Point de terminaison d'exécution d'action

Le point de terminaison d'exécution de l'action est l'endroit où se trouve l'essentiel de la logique de toute action. Nous allons y découvrir la logique spécifique au cas d'utilisation d'insertion BigQuery.

Dans nos déclarations de routes, nous avons rendu ce point de terminaison disponible sous le chemin d'accès /action-0/execute et lui avons associé trois gestionnaires: requireInstanceAuth, processRequestBody et action0Execute.

Nous avons déjà abordé requireInstanceAuth, et le gestionnaire processRequestBody fournit un prétraitement principalement inintéressant pour convertir certains champs incommodes du corps de la requête de Looker dans un format plus pratique. Vous pouvez toutefois vous y référer dans le fichier de code complet.

La fonction action0Execute commence par montrer des exemples d'extraction d'informations utiles à partir de plusieurs parties de la requête d'action. En pratique, notez que les éléments de requête que notre code appelle formParams et actionParams peuvent contenir différents champs, en fonction de ce que vous déclarez dans vos points de terminaison de fiche et de formulaire.

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,
            }

Le code passe ensuite à du code BigQuery standard pour insérer les données. Notez que les API BigQuery Storage Write proposent d'autres variantes plus complexes, plus adaptées à une connexion de streaming persistante ou à des insertions groupées de nombreux enregistrements. Toutefois, pour répondre aux interactions individuelles des utilisateurs dans le contexte d'une fonction Cloud Run, il s'agit de la variante la plus directe.

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()}
        }
    }

Le code de démonstration inclut également un point de terminaison "status" à des fins de dépannage, mais ce point de terminaison n'est pas obligatoire pour l'intégration de l'API Action.

Guide de déploiement

Enfin, nous vous fournirons un guide détaillé pour déployer la démonstration vous-même. Il couvrira les prérequis, le déploiement de la fonction Cloud Run, la configuration de BigQuery et la configuration de Looker.

Conditions préalables pour les projets et les services

Avant de commencer à configurer des éléments spécifiques, consultez cette liste pour comprendre les services et les règles dont la solution aura besoin:

  1. Un nouveau projet:vous aurez besoin d'un nouveau projet pour héberger les ressources de notre exemple.
  2. Services:lorsque vous utilisez pour la première fois les fonctions BigQuery et Cloud Run dans l'interface utilisateur de la console Cloud, vous êtes invité à activer les API requises pour les services nécessaires, y compris BigQuery, Artifact Registry, Cloud Build, Cloud Functions, Cloud Logging, Pub/Sub, Cloud Run Admin et Secret Manager.
  3. Règle pour les appels non authentifiés:ce cas d'utilisation nous oblige à déployer des fonctions Cloud Run qui autorisent les appels non authentifiés, car nous gérerons l'authentification des requêtes entrantes dans notre code conformément à l'API Action, plutôt qu'à l'aide d'IAM. Bien que cette pratique soit autorisée par défaut, les règles d'administration de l'organisation limitent souvent son utilisation. Plus précisément, la stratégie constraints/iam.allowedPolicyMemberDomains limite les personnes auxquelles des autorisations IAM peuvent être accordées. Vous devrez peut-être l'ajuster pour autoriser le principal allUsers à accéder de manière non authentifiée. Si vous ne parvenez pas à autoriser les invocations non authentifiées, consultez le guide Créer des services Cloud Run publics lorsque le partage restreint de domaine est appliqué pour en savoir plus.
  4. Autres règles:n'oubliez pas que d'autres contraintes de règles d'administrationGoogle Cloud peuvent également empêcher le déploiement de services autorisés par défaut.

Déployer la fonction Cloud Run

Une fois que vous avez créé un projet, procédez comme suit pour déployer la fonction Cloud Run :

  1. Dans Fonctions Cloud Run, cliquez sur Créer une fonction.
  2. Choisissez un nom pour votre fonction (par exemple, "demo-bq-insert-action").
  3. Sous les paramètres Déclencheur :
    1. Le type de déclencheur doit déjà être "HTTPS".
    2. Définissez Authentification sur Autoriser les appels non authentifiés.
    3. Copiez la valeur de l'URL dans le presse-papiers.
  4. Sous les paramètres Environnement d'exécution > Variables d'environnement d'exécution :
    1. Cliquez sur Ajouter une variable.
    2. Définissez le nom de la variable sur CALLBACK_URL_PREFIX.
    3. Collez l'URL de l'étape précédente en tant que valeur.
  5. Cliquez sur Suivant.
  6. Cliquez sur le fichier package.json, puis collez le contenu.
  7. Cliquez sur le fichier index.js, puis collez le contenu.
  8. Attribuez la variable projectId en haut du fichier à votre propre ID de projet.
  9. Définissez le point d'entrée sur httpHandler.
  10. Cliquez sur Déployer.
  11. Accordez les autorisations demandées (le cas échéant) au compte de service de compilation.
  12. Attendez la fin du déploiement.
  13. Si, lors d'étapes ultérieures, un message d'erreur vous invite à consulter les journaux Google Cloud , notez que vous pouvez y accéder depuis l'onglet Journaux de cette page.
  14. Avant de quitter la page de votre fonction Cloud Run, dans l'onglet Détails, recherchez et notez le compte de service associé à la fonction. Nous l'utiliserons dans les étapes suivantes pour nous assurer que la fonction dispose des autorisations dont elle a besoin.
  15. Testez le déploiement de votre fonction directement dans votre navigateur en accédant à l'URL. Vous devriez obtenir une réponse JSON contenant la fiche de votre intégration.
  16. Si vous recevez une erreur 403, il est possible que votre tentative de définir Allow unauthenticated invocations (Autoriser les appels non authentifiés) ait échoué de manière silencieuse en raison d'une règle d'administration. Vérifiez si votre fonction autorise les appels non authentifiés, examinez le paramètre de votre règle d'administration et essayez de le mettre à jour.

Accès à la table de destination BigQuery

En pratique, la table de destination dans laquelle vous souhaitez insérer les données peut se trouver dans un autre projet Google Cloud . Toutefois, à des fins de démonstration, nous allons créer une table de destination dans le même projet. Dans les deux cas, vous devez vous assurer que le compte de service de votre fonction Cloud Run est autorisé à écrire dans la table.

  1. Accédez à la console BigQuery.
  2. Créez la table de démonstration:

    1. Dans la barre "Explorateur", utilisez le menu à trois points à côté de votre projet, puis sélectionnez Créer un ensemble de données.
    2. Attribuez à votre ensemble de données l'ID demo_dataset, puis cliquez sur Créer un ensemble de données.
    3. Utilisez le menu à trois points de votre nouvel ensemble de données, puis sélectionnez Créer une table.
    4. Nommez votre table demo_table.
    5. Sous Schéma, sélectionnez Modifier sous forme de texte, utilisez le schéma suivant, puis cliquez sur Créer une table.

      [
       {"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. Attribuez des autorisations:

    1. Dans la barre Explorer, cliquez sur votre ensemble de données.
    2. Sur la page de l'ensemble de données, cliquez sur Partage > Autorisations.
    3. Cliquez sur Ajouter un compte principal.
    4. Définissez le Nouveau principal sur le compte de service de votre fonction, indiqué plus haut sur cette page.
    5. Attribuez le rôle Éditeur de données BigQuery.
    6. Cliquez sur Enregistrer.

Se connecter à Looker

Maintenant que votre fonction est déployée, nous allons l'associer à Looker.

  1. Nous aurons besoin d'un secret partagé pour que votre action puisse authentifier les requêtes provenant de votre instance Looker. Générez une longue chaîne aléatoire et protégez-la. Nous l'utiliserons dans les étapes suivantes comme valeur de secret Looker.
  2. Dans Cloud Console, accédez à Secret Manager.
    1. Cliquez sur Créer un secret.
    2. Définissez le paramètre Nom sur LOOKER_SECRET. (Ce nom est codé en dur dans le code de cette démonstration, mais vous pouvez choisir n'importe quel nom lorsque vous travaillez avec votre propre code.)
    3. Définissez la valeur secrète sur la valeur secrète que vous avez générée.
    4. Cliquez sur Créer un secret.
    5. Sur la page Secret, cliquez sur l'onglet Autorisations.
    6. Cliquez sur Accorder l'accès.
    7. Définissez Nouveaux membres sur le compte de service de votre fonction, indiqué précédemment.
    8. Attribuez le rôle Accesseur de secrets Secret Manager.
    9. Cliquez sur Enregistrer.
    10. Vous pouvez vérifier que votre fonction accède correctement au secret en accédant au routage /status ajouté à l'URL de votre fonction.
  3. Dans votre instance Looker :
    1. Accédez à "Admin" > "Plate-forme" > "Actions".
    2. En bas de la page, cliquez sur Ajouter un hub d'actions.
    3. Indiquez l'URL de votre fonction (par exemple, https://votre-région-votre-projet.cloudfunctions.net/demo-bq-insert-action), puis confirmez en cliquant sur Ajouter un hub d'actions.
    4. Une nouvelle entrée Action Hub s'affiche avec une action intitulée Démo d'insertion BigQuery.
    5. Dans l'entrée "Hub d'actions", cliquez sur Configurer l'autorisation.
    6. Saisissez le secret Looker généré dans le champ Authorization Token (Jeton d'autorisation), puis cliquez sur Update Token (Mettre à jour le jeton).
    7. Sur l'action Démo d'insertion BigQuery, cliquez sur Activer.
    8. Activez le bouton Activé.
    9. Un test de l'action devrait s'exécuter automatiquement, confirmant que votre fonction accepte la requête de Looker et répond correctement au point de terminaison du formulaire.
    10. Cliquez sur Enregistrer.

Test de bout en bout

Nous devrions maintenant pouvoir utiliser notre nouvelle action. Cette action est configurée pour fonctionner avec n'importe quelle requête. Choisissez une exploration (par exemple, une exploration intégrée sur l'activité système), ajoutez des champs à une nouvelle requête, exécutez-la, puis sélectionnez Envoyer dans le menu en forme de roue dentée. L'action devrait s'afficher parmi les destinations disponibles, et vous devriez être invité à saisir des informations dans les champs suivants:

Capture d'écran de la fenêtre modale "Envoyer" de Looker avec notre nouvelle action sélectionnée

Lorsque vous appuyez sur Envoyer, une nouvelle ligne doit être insérée dans votre table BigQuery (et l'adresse e-mail de votre compte utilisateur Looker doit être identifiée dans la colonne invoked_by).