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.
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.
| Type | Export | Signature |
|---|---|---|
| Événement de plateforme | onPlatformEvent | (event: PlatformEvent) => void | Promise<void> |
Hook contact.create.pre | onContactCreatePre | (input: ContactCreateInput) => HookOutput<ContactCreateResult> | Promise<…> |
Hook contact.verify | onContactVerify | (input: ContactVerifyInput) => HookOutput<ContactVerifyResult> | Promise<…> |
Les types viennent du module ./omnilab, disponible dans toute function :
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
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
datane contient pas decontact_idest 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.v1est refusé quand vous l'enregistrez. - La livraison se fait au moins une fois. Le même
idpeut 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.
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
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 :
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.
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.
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
| Appel | Retour | Remarques |
|---|---|---|
OmniLab.Config.get(key) | La valeur de la configuration du plugin, ou undefined | Configuration non sensible uniquement |
OmniLab.Config.getSecret("bundle.KEY") | La valeur du secret | Les 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
| Appel | Remarques |
|---|---|
OmniLab.Http.fetch(url, options?) | La forme générale |
OmniLab.Http.get / post / put / patch / delete | Dé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 :
| Option | Type | Remarques |
|---|---|---|
method | string | GET si vous l'omettez |
headers | Record<string, string> | Envoyés tels quels. Aucun Content-Type n'est ajouté automatiquement. |
body | string | Texte 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 :
| Champ | Type | Remarques |
|---|---|---|
ok | boolean | true pour un statut 2xx |
status | number | Le statut HTTP. Un 4xx ou un 5xx se résout normalement au lieu de lever une exception. |
statusText | string | Le libellé du statut seul, comme Not Found |
headers | Record<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 :
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
| Appel | Retour |
|---|---|
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 :
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.
| Code | Réessayable | Cause typique |
|---|---|---|
NOT_FOUND | Non | Une clé de secret inexistante (les clés sont sensibles à la casse), une function sans lot de secrets associé, ou un contact inexistant |
PERMISSION_DENIED | Non | getSecret désigne un lot de secrets qui n'est pas associé à cette function |
INVALID_ARGUMENT | Non | Une 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_EXISTS | Non | Une écriture en conflit |
EGRESS_BLOCKED | Non | Le nom d'hôte n'est pas autorisé, ou résout vers une adresse privée |
RESOURCE_EXHAUSTED | Non | Le 21e appel SDK d'une même invocation. L'invocation est arrêtée : intercepter l'erreur ne sert à rien. |
TIMEOUT | Oui | Une requête HTTP a dépassé sa limite de 1500 ms |
UNAVAILABLE | Oui | Une dépendance était momentanément injoignable |
INTERNAL | Oui | Une 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
| Limite | Valeur |
|---|---|
| Délai | 50 – 2000 ms, 2000 par défaut |
| Mémoire | 4 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 SDK | 20 |
| Délai d'une requête HTTP | 1500 ms |
| Corps de réponse HTTP | 4 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
| Limite | Valeur |
|---|---|
| Taille de la source | 1 Mo |
| Hôtes autorisés | 16 |
| Configuration du plugin | 32 clés, 4 Ko au total |
| Lots de secrets associés | 5 |
| Compilations simultanées | 1 |
Pour l'ensemble de vos functions
| Limite | Valeur |
|---|---|
| Abonnements actifs par type d'événement | 5 |
| Liaisons de hook actives par point d'accroche | 1 globale, plus 1 par organisation |
| Lots de secrets | 5 |
| Secrets par lot de secrets | 32 |
| Clé de lot de secrets | Jusqu'à 32 caractères : lettres minuscules, chiffres et tirets bas, en commençant par une lettre |
| Clé de secret | Jusqu'à 64 caractères : lettres, chiffres et tirets bas, en commençant par une lettre. Sensible à la casse |
Conservation et troncature
| Élément | Valeur |
|---|---|
| Exécutions | 60 jours |
| Entrée et sortie conservées | 256 Ko chacune, tronquées au-delà, avec les tailles d'origine indiquées |
| Journaux capturés | 16 Ko au total, 256 lignes, 2 Ko par ligne |
| Journal de compilation affiché dans Studio | Les 8 derniers Ko |
Compilation
| Limite | Valeur |
|---|---|
| Vérification des types lors de la sauvegarde | 10 secondes |
| Compilation | 3 minutes |
| Libération d'une compilation bloquée, après quoi vous pouvez en lancer une autre | 15 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
Tester et superviser des functions
Lancez des invocations de test dans l'environnement isolé réel, lisez le journal d'exécution et diagnostiquez les functions en échec ou silencieuses.
Changelog API
Comprenez comment l'API est versionnée aujourd'hui, et ce qu'il faut surveiller tant qu'aucun changelog API public n'est publié.