Functions de type hook

Écrire une function qui s'exécute au cœur d'une opération OmniLab et décide si elle se poursuit, puis l'associer à un point d'ancrage.

8 min de lecture

Une function de type hook s'exécute pendant une opération, et non après. OmniLab suspend son traitement, appelle votre code, et applique votre réponse : poursuivre, éventuellement avec des valeurs que vous fournissez, ou refuser. Ce pouvoir explique pourquoi les functions hook demandent plus de précautions que les functions d'événement.

Quand l'utiliser

Choisissez une function hook lorsque l'issue d'une opération OmniLab doit dépendre de votre logique : bloquer les inscriptions provenant de domaines e-mail jetables, normaliser des données avant leur enregistrement, ou envoyer les e-mails de vérification via votre propre fournisseur. Si vous voulez seulement réagir à un événement passé, utilisez plutôt une function d'événement.

L'interface Functions est uniquement en anglais

Les écrans Functions ne sont pas traduits ; les libellés sont donc cités en anglais dans cet article.

Les deux points d'ancrage

Une function hook est créée pour un seul point d'ancrage et ne pourra jamais être associée qu'à celui-là.

Point d'ancrageSe déclencheContinue permet deDeny provoque
contact.create.preAvant l'enregistrement d'un contact, depuis une inscription, l'admin, un import ou l'APIRéécrire l'e-mail, le numéro de téléphone et les champs personnalisésLe refus du contact
contact.verificationLorsqu'un message de vérification en double opt-in doit être envoyéRemonter l'identifiant de message du fournisseur, à titre de journalisationL'échec de l'inscription

Un hook est sur le chemin critique

Dès qu'une association est active, chaque opération couverte attend votre function, et si celle-ci ne peut pas être évaluée, l'opération est refusée. Une erreur, un dépassement de délai et une valeur de retour non conforme au contrat sont tous traités comme un refus. Pour contact.create.pre, cela signifie l'arrêt des inscriptions des organisations couvertes. Testez avant d'activer, et gardez un délai serré.

Les deux décisions

Construisez toujours votre valeur de retour avec OmniLab.Flow. Retourner un objet ordinaire constitue une violation de contrat, et une violation de contrat vaut refus.

Les deux seules choses qu'un hook peut retourner
OmniLab.Flow.continue(result?)      // poursuivre, en fournissant éventuellement des valeurs
OmniLab.Flow.deny(code, message)    // bloquer l'opération

contact.create.pre

Votre fichier exporte onContactCreatePre.

user.ts — hello world
import { OmniLab } from "./omnilab";
import type { ContactCreatePreHandler } from "./omnilab";

export const onContactCreatePre: ContactCreatePreHandler = (input) => {
  console.log("Hello world ! Création d'un contact : " + input.contact.email);
  return OmniLab.Flow.continue();
};

Cette version laisse passer tous les contacts sans modification, ce qui en fait la bonne première étape : associez-la, regardez les exécutions apparaître, et confirmez que les inscriptions fonctionnent toujours avant d'ajouter une logique capable de refuser.

Refuser et réécrire

continue() sans argument ne change rien. Passez un objet pour qu'OmniLab applique des valeurs — seuls les champs retournés sont appliqués, donc en omettre un le laisse exactement tel qu'il a été soumis.

user.ts — normaliser l'e-mail, bloquer un domaine
import { OmniLab } from "./omnilab";
import type { ContactCreatePreHandler } from "./omnilab";

export const onContactCreatePre: ContactCreatePreHandler = (input) => {
  const email = input.contact.email.trim().toLowerCase();

  if (email.endsWith("@blocked.example")) {
    return OmniLab.Flow.deny("blocked_domain", "Ce domaine e-mail n'est pas accepté.");
  }

  // Seul `email` est retourné : le téléphone et les champs personnalisés sont enregistrés tels quels.
  return OmniLab.Flow.continue({ email });
};

Le code et le message du refus sont remontés à l'appelant : rédigez un message sur lequel une personne peut agir.

La normalisation vaut la peine ici parce que le hook s'exécute avant l'enregistrement du contact et avant la mise à jour de tout CRM connecté : l'e-mail que vous retournez est celui qu'OmniLab persiste et transmet, et le passer en minuscules évite donc que Foo@example.com et foo@example.com deviennent deux profils.

Quand votre fournisseur est indisponible

Si votre gestionnaire appelle un tiers, décidez explicitement de ce qui se passe lorsque ce tiers tombe. Les hooks échouant en mode fermé, ne rien faire signifie qu'une panne du fournisseur arrête les inscriptions de toutes les organisations couvertes par l'association.

Interceptez l'échec et poursuivez. Un fournisseur injoignable ne prouve pas qu'un client est un fraudeur :

user.ts — refuser les mauvaises adresses, mais survivre à une panne
import { OmniLab } from "./omnilab";
import type { ContactCreatePreHandler } from "./omnilab";

export const onContactCreatePre: ContactCreatePreHandler = async (input) => {
  const email = input.contact.email.trim().toLowerCase();

  try {
    const response = await OmniLab.Http.get(
      "https://api.example-checker.com/v1/check?email=" + encodeURIComponent(email),
    );
    const verdict = await response.json<{ disposable: boolean }>();

    if (verdict.disposable) {
      return OmniLab.Flow.deny("disposable_email", "Merci d'utiliser une adresse e-mail permanente.");
    }
  } catch (error) {
    // Le service de vérification est injoignable. On laisse passer l'inscription
    // plutôt que de bloquer un vrai client à cause de la panne d'un tiers : une
    // inscription non vérifiée coûte beaucoup moins cher qu'une porte fermée.
    console.warn("vérification indisponible, inscription autorisée : " + String(error));
  }

  return OmniLab.Flow.continue({ email });
};

Ne refusez que sur une réponse négative certaine. Traitez « je n'ai pas pu obtenir de réponse » comme une raison de poursuivre.

Un dépassement de délai du gestionnaire ne peut pas être intercepté

try/catch vous protège d'une erreur HTTP, pas d'un manque de temps : lorsque le délai du gestionnaire expire, l'invocation est interrompue et l'opération est refusée, sans possibilité de récupération. C'est pourquoi le délai doit conserver de la marge au-dessus de la traîne lente de votre dépendance, au lieu d'être calé sur sa médiane.

contact.verification

Votre fichier exporte onContactVerification. Ce point d'ancrage est le mécanisme par lequel OmniLab envoie les messages de vérification en double opt-in — c'est votre gestionnaire qui est responsable de leur livraison.

Commencez par valider le câblage sans fournisseur. Le lien de vérification arrive dans input.verification_url : journalisez-le.

user.ts — hello world, pour la mise en place uniquement
import { OmniLab } from "./omnilab";
import type { ContactVerificationHandler } from "./omnilab";

export const onContactVerification: ContactVerificationHandler = (input) => {
  console.log("Hello world ! Lien de vérification pour " + input.contact.email);
  console.log(input.verification_url);
  return OmniLab.Flow.continue();
};

Inscrivez un contact de test, ouvrez le journal d'exécution, et copiez le lien depuis le panneau Logs pour terminer la vérification à la main.

À remplacer avant la mise en production

Poursuivre sans rien envoyer indique à OmniLab que le message a été expédié alors qu'il ne l'a pas été. De vrais contacts ne recevraient jamais de lien. N'utilisez cette version que pendant la mise en place.

Envoyer via votre fournisseur

Ajoutez l'appel au fournisseur, et transmettez input.context.idempotency_key pour qu'une tentative répétée n'envoie pas deux fois. OmniLab ne réessaie jamais l'invocation d'un hook, mais l'inscription qui l'entoure peut être retentée — un client qui resoumet le formulaire, ou votre site qui relance l'appel — et la clé reste stable d'une tentative à l'autre : votre fournisseur peut donc écarter le doublon. Un renvoi explicite porte volontairement une clé différente, afin qu'un contact qui redemande un lien en reçoive bien un.

C'est le seul point d'ancrage qui fournit une clé. contact.create.pre en reçoit une vide : un gestionnaire qui y appelle un fournisseur payant doit donc définir sa propre valeur de dédoublonnage.

user.ts — envoi avec Brevo
import { OmniLab } from "./omnilab";
import type { ContactVerificationHandler } from "./omnilab";

export const onContactVerification: ContactVerificationHandler = async (input) => {
  const response = await OmniLab.Http.post("https://api.brevo.com/v3/smtp/email", {
    headers: {
      "content-type": "application/json",
      "api-key": OmniLab.Config.getSecret("brevo.api_key"),
    },
    body: JSON.stringify({
      to: [{ email: input.contact.email, name: input.contact.firstname }],
      templateId: 1,
      params: {
        FIRSTNAME: input.contact.firstname,
        VERIFICATION_URL: input.verification_url,
      },
      // Transmise pour que le fournisseur dédoublonne un réessai.
      headers: { "X-Mailin-Custom": input.context.idempotency_key },
    }),
  });

  if (!response.ok) {
    return OmniLab.Flow.deny("dispatch_failed", "Impossible d'envoyer l'e-mail de vérification.");
  }

  const sent = await response.json<{ messageId?: string }>();
  return OmniLab.Flow.continue({ provider_message_id: sent.messageId ?? "" });
};

Deux prérequis pour que cela fonctionne : api.brevo.com dans la liste d'hôtes autorisés de la function, et un bundle de secrets brevo qui lui est associé. Les deux sont traités dans Configurer l'exécution et les secrets.

Continuer signifie expédié, pas délivré

continue indique à OmniLab que votre fournisseur a accepté le message. Cela ne dit rien de son arrivée. Ne refusez que lorsque l'expédition elle-même a échoué, car refuser fait échouer l'inscription.

L'associer au point d'ancrage

Ouvrez la function, utilisez l'onglet Hooks, puis sélectionnez Add binding.

Le Hook point est figé et affiché en lecture seule. Ce que vous choisissez, c'est la portée :

PortéeQuand l'utiliserComment la définir
GlobaleLa function doit s'exécuter pour toutes les organisations du tenantActiver Global (all groups)
Organisations précisesSeules certaines organisations ont besoin de cette logiqueLaisser l'interrupteur désactivé et sélectionner Target groups

L'interface applique les règles de portée pour vous :

  • Au maximum une association globale active par point d'ancrage. S'il en existe une, l'interrupteur global est désactivé et l'explique.
  • Une organisation appartient à au maximum une association active par point d'ancrage. Les organisations déjà couvertes apparaissent grisées.
  • Une association ciblée l'emporte sur l'association globale pour les organisations qu'elle nomme. Utilisez une association globale pour le comportement par défaut, et des associations ciblées pour les exceptions.

Formulaire Add binding avec le point d’ancrage figé, l’interrupteur global et les organisations ciblées

Sélectionnez Create. L'association démarre en Active et prend effet immédiatement — il n'existe pas d'association en brouillon ni en mode journalisation seule : le clic lui-même place votre code dans le parcours réel.

Les règles de portée sont donc votre outil de déploiement progressif. Commencez par associer une seule organisation à faible trafic, laissez tourner une journée, lisez le journal d'exécution, et n'élargissez à une association globale qu'ensuite. Élargir suppose de supprimer et recréer, ce qui est peu coûteux ; découvrir un gestionnaire défaillant sur toutes les organisations à la fois ne l'est pas.

Gérer une association

Chaque ligne propose Disable et Delete. Désactiver conserve la configuration mais interrompt le routage : c'est ainsi que vous retirez votre logique d'un parcours en production en un clic. Supprimer la retire, et le routage cesse immédiatement.

Il n'y a pas d'action d'édition — pour modifier les organisations couvertes, supprimez l'association et créez-en une nouvelle.

La carte de la function affiche sa portée d'un coup d'œil : Global, le nom des organisations ciblées, ou unbound lorsqu'une function hook n'a aucune association active.

Le double opt-in exige une association existante

Si le double opt-in est activé pour une organisation et qu'aucune function n'est associée à contact.verification, les inscriptions de cette organisation échouent : rien ne peut envoyer le message de vérification. Associez la function avant d'activer le double opt-in, pas après.

Sémantique des échecs

Les hooks échouent en mode fermé, de façon uniforme. Il n'y a aucun réessai : l'opération attend, il n'y a donc pas de temps pour cela.

Ce qui s'est passéCe que fait OmniLab
Vous avez retourné denyBloque l'opération avec votre code et votre message. C'est une invocation normale et réussie.
Votre gestionnaire a échoué ou dépassé son délaiBloque l'opération
Votre valeur de retour ne respectait pas le contratBloque l'opération
Aucune association active ne couvre cette organisationApplique le comportement par défaut, comme si aucune function n'existait

Cette dernière ligne est le filet de sécurité à retenir : désactiver une association rétablit instantanément le comportement par défaut, et c'est la première chose à faire si un hook provoque des échecs.

Si quelque chose bloque

L'onglet Hooks est absent. Il n'apparaît que sur les functions hook. Vérifiez le badge de type en haut de l'éditeur.

L'interrupteur global est désactivé. Une autre association globale active couvre déjà ce point d'ancrage. Désactivez-la, ou ciblez des organisations précises.

Une organisation est grisée dans le sélecteur. Elle est déjà couverte par une autre association active sur ce point d'ancrage.

Les inscriptions échouent depuis l'activation d'une association. Désactivez l'association pour rétablir le comportement par défaut, puis cherchez la raison dans le journal d'exécution. Une Contract violation plutôt qu'un refus volontaire signifie que la forme de votre valeur de retour est incorrecte.

Votre test affiche une violation de contrat. Vous avez retourné un objet ordinaire au lieu de passer par OmniLab.Flow, refusé sans code, ou fourni un champ de résultat que ce point d'ancrage ne définit pas.

L'e-mail de vérification n'arrive jamais. Vérifiez que l'appel sortant a réussi dans le détail de l'exécution. Une erreur EGRESS_BLOCKED signifie que l'hôte du fournisseur n'est pas autorisé ; une erreur NOT_FOUND signifie que le bundle de secrets n'est pas associé ou que le nom de clé est incorrect.

Pour aller plus loin

Sur cette page