Référence du SDK des functions

Exports du gestionnaire, structures d'entrée et de sortie, appels SDK, contrats des hooks, codes d'erreur et toutes les limites.

12 min de lecture

Le contrat complet entre votre code TypeScript et l'environnement d'exécution d'OmniLab.

Exports du gestionnaire

Votre fichier doit exporter exactement un gestionnaire, nommé d'après le type de la function. La sauvegarde vérifie cet export par rapport à la signature attendue pour ce type de function : avec un gestionnaire manquant ou mal formé, c'est la sauvegarde qui échoue, pas la function en production.

TypeExportSignature
Événement de plateformeonPlatformEvent(event: PlatformEvent) => void | Promise<void>
Hook contact.create.preonContactCreatePre(input: ContactCreateInput) => HookOutput<ContactCreateResult> | Promise<…>
Hook contact.verifyonContactVerify(input: ContactVerifyInput) => HookOutput<ContactVerifyResult> | Promise<…>

Les types viennent du module ./omnilab, disponible dans toute function :

Importing the SDK and its types
import { OmniLab, contactDisplayName } from "./omnilab";
import type { PlatformEventHandler, ContactCreatePreHandler } from "./omnilab";

OmniLab est l'objet SDK. contactDisplayName est une fonction utilitaire pure qui résout le nom d'un contact : voir les deux formats de nom.

Les gestionnaires peuvent être async. Seul TypeScript est pris en charge, dans un fichier unique dont le point d'entrée est user.ts.

Structures d'entrée

PlatformEvent

What a platform-event function receives
type PlatformEvent = {
  type: string;   // par ex. "touchpoint.completed.v1"
  id: string;     // unique par événement : dédoublonnez sur cette valeur
  time: string;   // RFC 3339
  source: string;
  data: Json;     // le payload de l'événement, exactement tel qu'OmniLab l'a publié
};

Payloads des événements

data contient les champs de l'événement, qui varient selon le type d'événement. La référence des événements les liste pour chaque type, et indique quels types atteignent une function. Castez data vers le type attendu, et considérez chaque champ comme potentiellement absent.

Livraison des événements de plateforme

Ces règles s'appliquent en plus de la sélection faite par l'abonnement :

  • Seuls les événements avec un contact identifié déclenchent une function. Un événement dont data ne contient pas de contact_id est ignoré : l'activité d'un visiteur anonyme ne déclenche donc jamais de function.
  • Les visites de page ne déclenchent jamais de function. Un abonnement qui inclut touchpoint.page_visit.v1 est refusé quand vous l'enregistrez.
  • La livraison se fait au moins une fois. Le même id peut atteindre votre gestionnaire plus d'une fois.

Donner deux fois la même valeur à un champ de contact est sans conséquence. Deux choses ne le sont pas : appeler une API qui facture, envoie un message ou attribue un bon d'achat, et ajouter une valeur à un champ que vous venez de lire, ce qui peut faire perdre l'une des deux écritures. Dédoublonnez à partir de event.id avant l'une ou l'autre.

Contrats des hooks

Chaque entrée de hook comporte le même objet context : des métadonnées sur l'invocation, quel que soit le point d'accroche. Il est toujours présent, et un champ qu'OmniLab n'a pas rempli est une chaîne vide.

HookContext, on every hook input
interface HookContext {
  idempotency_key: string;         // une nouvelle clé à chaque envoi de vérification ; vide sur contact.create.pre
  group_id: string;                // l'organisation pour laquelle la liaison a été résolue
  interaction_id: string;          // la campagne à l'origine de l'opération, par ex. "interactions/<id>"
  interaction_public_key: string;  // la clé publique du point d'entrée de cette campagne
}

group_id, interaction_id et interaction_public_key indiquent pour quelle organisation la liaison a acheminé l'appel, et de quelle campagne provient l'opération. Une inscription via une campagne remplit les trois. Un contact créé par un administrateur, par un import ou via l'API n'est rattaché à aucune campagne : les champs d'interaction restent vides.

Seul contact.verify remplit idempotency_key. OmniLab ne réessaie jamais un appel de hook, et chaque envoi, y compris un renvoi, reçoit une nouvelle clé. Transmettez-la à un fournisseur qui en accepte une.

contact.create.pre reçoit une clé vide. Si votre gestionnaire appelle un fournisseur qui facture ou modifie quelque chose, fournissez votre propre valeur de dédoublonnage : l'email du contact est le choix habituel.

Ce que retourne un gestionnaire de hook

HookOutput, from ./omnilab
type HookOutput<T> =
  | { decision: "continue"; result?: T }
  | { decision: "deny"; code: string; message: string };

OmniLab.Flow.continue(result?) et OmniLab.Flow.deny(code, message) construisent ces valeurs, et un simple objet de même structure est également accepté. Un refus avec un code vide est signalé comme DENIED.

Le contrat est rompu quand le gestionnaire ne retourne rien, utilise une autre decision, ou retourne un champ result que le point d'accroche ne définit pas ou dont le type est incorrect. Un contrat rompu compte comme un échec. L'effet de chaque cas est décrit dans Ce que garantit un hook.

Deux formats de nom possibles

Les deux points d'accroche vous transmettent un contact dont le nom arrive dans l'un des deux formats, fixé par la campagne dont il provient :

  • firstname + lastname : ce qu'un formulaire d'inscription recueille par défaut ;
  • fullname : une seule chaîne, pour les campagnes dont le formulaire recueille un seul champ Nom complet.

Jamais les deux. Le format que le contact n'utilise pas est vide : lire firstname seul laisse donc la salutation vide pour une campagne qui recueille un nom complet, et firstname + " " + lastname produit une espace isolée. Utilisez la fonction utilitaire au lieu de concaténer :

Resolving a display name
import { contactDisplayName } from "./omnilab";

const name = contactDisplayName(input.contact);

Elle essaie fullname, puis firstname + lastname, puis email, et renvoie une chaîne vide quand le contact n'a aucun nom exploitable. N'affichez alors rien, ou votre propre texte de remplacement, mais jamais contact.id, un identifiant opaque de la forme contacts/<uuid>.

Les noms sont en lecture seule depuis un hook. Aucun des deux types de résultat ne comporte de champ de nom : une function ne peut donc réécrire le nom dans aucun des deux formats.

contact.create.pre

S'exécute avant qu'un contact soit stocké, qu'il provienne d'une inscription, d'un administrateur, d'un import ou de l'API.

ContactCreateInput
interface ContactCreateInput {
  contact: {
    email: string;
    phone_number: string;
    firstname?: string;
    lastname?: string;
    fullname?: string;                 // renseigné à la place de firstname et lastname : voir ci-dessus
    custom_fields?: Record<string, string>;
    optin?: boolean;
  };
  source: "signup" | "admin" | "import" | "api";
  context: HookContext;                // idempotency_key est vide pour ce point d'accroche
}

La décision continue accepte un email, un phone_number et des custom_fields, tous facultatifs. Seuls les champs que vous retournez sont appliqués : omettez-en un pour le laisser tel que soumis.

optin est le consentement marketing indiqué par le visiteur. Il est toujours envoyé : false signifie donc que le visiteur n'a pas donné son accord, jamais « inconnu ». Il est en lecture seule : le résultat ne comporte pas de champ optin, et une function ne peut donc pas réécrire le consentement.

source est typé avec quatre valeurs, mais chaque appel envoie "signup", quelle que soit l'origine du contact. Ne vous en servez pas dans une condition.

contact.verify

S'exécute quand une inscription en double opt-in doit recevoir son email de vérification. C'est votre gestionnaire qui l'envoie.

ContactVerifyInput
interface ContactVerifyInput {
  contact: {
    email: string;
    phone_number?: string;
    firstname?: string;
    lastname?: string;
    fullname?: string;                 // renseigné à la place de firstname et lastname : voir ci-dessus
    optin?: boolean;
    custom_fields?: Record<string, string>;
  };
  verification_url: string;            // le lien vers lequel votre email doit pointer
  expires_at: string;                  // moment RFC 3339 où le lien cesse de fonctionner
  context: HookContext;                // les quatre champs sont remplis pour une inscription via une campagne
}

custom_fields contient les réponses supplémentaires du formulaire d'inscription, telles que soumises : vous pouvez vous en servir pour personnaliser le message. optin peut orienter le choix du modèle envoyé, mais ne dispense pas d'envoyer le lien : l'envoi de verification_url reste obligatoire.

La décision continue accepte un provider_message_id facultatif, qui n'apparaît que dans le journal d'exécution, et un already_verified facultatif. Ne définissez already_verified: true que si vos données contiennent un opt-in confirmé pour l'adresse : voir Ne pas envoyer l'email.

coupon.assign n'est pas disponible

Un point d'accroche coupon.assign apparaît dans l'interface avec la mention « bientôt disponible », et le SDK contient ses types, mais toute liaison, invocation ou test est refusé. Ne développez rien qui en dépende.

Appels du SDK

Ce sont les seuls appels qui sortent de l'environnement isolé. Tout le reste de votre gestionnaire est du calcul pur.

Config

AppelRetourRemarques
OmniLab.Config.get(key)La valeur de la configuration du plugin, ou undefinedConfiguration non sensible uniquement
OmniLab.Config.getSecret("bundle.KEY")La valeur du secretLes clés sont sensibles à la casse. Lève PERMISSION_DENIED quand le lot de secrets n'est pas associé à la function, et NOT_FOUND quand la clé n'existe pas.

Http

AppelRemarques
OmniLab.Http.fetch(url, options?)La forme générale
OmniLab.Http.get / post / put / patch / deleteDéfinissent la méthode pour vous. get et delete ne prennent pas de body.

Les options sont un sous-ensemble des options de fetch du web :

OptionTypeRemarques
methodstringGET si vous l'omettez
headersRecord<string, string>Envoyés tels quels. Aucun Content-Type n'est ajouté automatiquement.
bodystringTexte uniquement : sérialisez le JSON vous-même avec JSON.stringify

Dès qu'une requête obtient une réponse, la promesse se résout avec cette réponse, quel que soit son statut :

ChampTypeRemarques
okbooleantrue pour un statut 2xx
statusnumberLe statut HTTP. Un 4xx ou un 5xx se résout normalement au lieu de lever une exception.
statusTextstringLe libellé du statut seul, comme Not Found
headersRecord<string, string>Noms en minuscules. Les valeurs d'un en-tête répété sont jointes par , .
text()Promise<string>Le corps sous forme de texte
json<T>()Promise<T>Le corps analysé en JSON. Lève une exception dès l'appel quand le corps n'est pas du JSON valide.

json() lève l'exception immédiatement au lieu de retourner une promesse rejetée : response.json().catch(...) ne voit donc jamais un corps invalide. Un corps vide, comme un 201 ou un 204 sans contenu, n'est pas non plus du JSON valide. Appelez-la dans un try/catch :

user.ts: post JSON and read the answer
import { OmniLab } from "./omnilab";
import type { PlatformEventHandler } from "./omnilab";

type WithContact = { contact?: { contact_id?: string } };

export const onPlatformEvent: PlatformEventHandler = async (event) => {
  const contactId = (event.data as WithContact).contact?.contact_id;

  const response = await OmniLab.Http.post("https://crm.lindenhall.example/api/visits", {
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + OmniLab.Config.getSecret("crm.API_TOKEN"),
    },
    body: JSON.stringify({ contactId, eventId: event.id }),
  });

  if (!response.ok) {
    throw new Error("CRM answered " + response.status + " " + response.statusText);
  }
  if (response.status === 204) {
    return; // accepté, rien à lire
  }

  try {
    const visit = await response.json<{ id: string }>();
    console.log("CRM visit " + visit.id);
  } catch {
    console.warn("CRM answered " + response.status + " without a JSON body");
  }
};

Chaque requête est vérifiée par rapport à la liste d'hôtes autorisés de la function, et les adresses internes ou privées sont refusées, qu'elles y figurent ou non. Seuls http et https sont autorisés. Une requête qui échoue avant toute réponse est rejetée avec un code d'erreur : EGRESS_BLOCKED, TIMEOUT ou UNAVAILABLE. Un corps de réponse au-delà de la limite de taille est tronqué, et la réponse comporte l'en-tête x-omnilab-response-truncated: true.

Contact

AppelRetour
OmniLab.Contact.get(id)Promise<Contact>
OmniLab.Contact.setCustomField(id, key, value)Promise<Contact> : le contact mis à jour
OmniLab.Contact.setCustomFields(id, fields)Promise<Contact> : tout ou rien

Les trois se résolvent avec le même objet : une écriture vous renvoie le contact qu'elle vient de modifier, et une relecture est donc rarement utile :

Contact
interface Contact {
  id: string;                              // "contacts/<uuid>"
  firstname: string;                       // l'un des deux formats de nom : voir ci-dessous
  lastname: string;
  fullname: string;
  email: string;
  phoneNumber: string;
  externalId: string;
  barcode: string;
  customFields: Record<string, string>;    // la seule partie modifiable
  emailVerified: boolean;
  optin: boolean;
  hasAcceptedTerms: boolean;
  isBlacklisted: boolean;
  blacklistReason: string;
  originInteraction: string;
}

Tous les champs sauf customFields sont en lecture seule depuis une function. Les valeurs des champs personnalisés sont toujours des chaînes.

Un contact lu ici présente les deux mêmes formats de nom qu'une entrée de hook : firstname + lastname, ou fullname, jamais les deux. Obtenez le nom avec contactDisplayName(contact) au lieu de lire chaque partie.

Les clés de champs personnalisés commençant par system. ou feature. sont réservées et refusées avec INVALID_ARGUMENT. Une clé peut faire jusqu'à 128 caractères et une valeur jusqu'à 4 Ko. Pour écrire plusieurs clés, préférez setCustomFields : un seul appel est décompté de votre budget au lieu de plusieurs.

Journalisation

console.log, console.info, console.warn, console.error, console.debug et console.trace sont capturés avec chaque exécution et affichés dans l'onglet Tester. La capture est limitée à 16 Ko au total, 256 lignes et 2 Ko par ligne ; au-delà, le journal est marqué comme tronqué.

Concevoir dans les limites du SDK

Les appels ci-dessus constituent l'ensemble du SDK. Si votre logique a besoin de plus, appelez vos propres systèmes en HTTP et effectuez le traitement de leur côté. Trois conséquences sont à prévoir :

Gardez votre état hors de la function. Chaque invocation repart de zéro : une valeur affectée au niveau du module a disparu à l'invocation suivante. Conservez ce dont vous avez besoin dans les champs personnalisés du contact, ou dans votre propre service.

Accédez aux autres données OmniLab via l'API. Les functions lisent et écrivent des contacts. Pour les transactions, les récompenses, les Smart Links et tout le reste, appelez l'API depuis vos propres systèmes.

Les functions s'exécutent sur déclencheur, pas à heure fixe. Impossible d'en planifier une. Si un traitement doit avoir lieu à un moment précis, lancez-le depuis votre propre planificateur.

Codes d'erreur

Les échecs du SDK lèvent une OmniLabError comportant un code, un message et un indicateur retryable. Importez le type depuis ./omnilab et lisez (error as OmniLabError).code.

CodeRéessayableCause typique
NOT_FOUNDNonUne clé de secret inexistante (les clés sont sensibles à la casse), une function sans lot de secrets associé, ou un contact inexistant
PERMISSION_DENIEDNongetSecret désigne un lot de secrets qui n'est pas associé à cette function
INVALID_ARGUMENTNonUne clé de champ personnalisé réservée, une valeur mal formée, ou une référence de secret qui n'a pas la forme bundle.KEY
ALREADY_EXISTSNonUne écriture en conflit
EGRESS_BLOCKEDNonLe nom d'hôte n'est pas autorisé, ou résout vers une adresse privée
RESOURCE_EXHAUSTEDNonLe 21e appel SDK d'une même invocation. L'invocation est arrêtée : intercepter l'erreur ne sert à rien.
TIMEOUTOuiUne requête HTTP a dépassé sa limite de 1500 ms
UNAVAILABLEOuiUne dépendance était momentanément injoignable
INTERNALOuiUne erreur inattendue de la plateforme

Le type OmniLabErrorCode du SDK liste aussi RATE_LIMITED et QUOTA_EXCEEDED, mais aucun appel SDK ne les lève.

Les messages d'erreur sont destinés à une personne qui lit le journal d'exécution, et leur formulation n'est pas stable. Un message TIMEOUT ou UNAVAILABLE indique l'appel, l'hôte pour une requête HTTP, et la cause : Http.fetch POST crm.lindenhall.example failed: connection refused, ou Contact.get failed: contact service unreachable. Ne les analysez jamais : testez plutôt le code.

Comportement en cas d'échec

  • Hooks : ce qui se passe en cas de refus, d'erreur, de dépassement de délai ou de liaison manquante, pour chaque point d'accroche, figure dans Ce que garantit un hook.
  • Événements de plateforme : l'activité à l'origine de l'événement n'est jamais affectée. Les nouvelles tentatives sont décrites dans Échecs et réessais.

Limites

Par invocation

LimiteValeur
Délai50 – 2000 ms, 2000 par défaut
Mémoire4 Mio pour chaque gestionnaire. Le champ Pages mémoire maximum accepte de 1 à 64 pages de 64 Kio (32 par défaut), mais ce réglage n'est pas encore appliqué
Appels SDK20
Délai d'une requête HTTP1500 ms
Corps de réponse HTTP4 Mio, tronqué au-delà

Le délai ne couvre que votre gestionnaire. Le temps qu'OmniLab passe à préparer son exécution n'est pas décompté de votre limite : le détail de l'exécution indique les deux séparément.

Par function

LimiteValeur
Taille de la source1 Mo
Hôtes autorisés16
Configuration du plugin32 clés, 4 Ko au total
Lots de secrets associés5
Compilations simultanées1

Pour l'ensemble de vos functions

LimiteValeur
Abonnements actifs par type d'événement5
Liaisons de hook actives par point d'accroche1 globale, plus 1 par organisation
Lots de secrets5
Secrets par lot de secrets32
Clé de lot de secretsJusqu'à 32 caractères : lettres minuscules, chiffres et tirets bas, en commençant par une lettre
Clé de secretJusqu'à 64 caractères : lettres, chiffres et tirets bas, en commençant par une lettre. Sensible à la casse

Conservation et troncature

ÉlémentValeur
Exécutions60 jours
Entrée et sortie conservées256 Ko chacune, tronquées au-delà, avec les tailles d'origine indiquées
Journaux capturés16 Ko au total, 256 lignes, 2 Ko par ligne
Journal de compilation affiché dans StudioLes 8 derniers Ko

Compilation

LimiteValeur
Vérification des types lors de la sauvegarde10 secondes
Compilation3 minutes
Libération d'une compilation bloquée, après quoi vous pouvez en lancer une autre15 minutes

Il n'y a aucune limite de débit sur les invocations. Le volume est encadré par le budget d'appels SDK, le plafond d'abonnements et l'exclusion des événements de visite de page anonymes.

Étapes suivantes

Sur cette page