À ce stade de la série, nous avons accompli quelque chose d'assez satisfaisant. Un client ouvre une conversation de soutien dans votre application. ColdFusion la stocke. Votre application la publie dans Slack. Votre équipe de soutien répond depuis Slack sans jamais ouvrir votre application.
Du point de vue de votre personnel de soutien, la vie est belle. Du point de vue de votre client, toutefois, rien ne s'est passé. C'est parce que Slack sait que quelqu'un a répondu. Votre application, elle, ne le sait pas.
L'un des plus grands changements de perspective lorsqu'on construit des systèmes pilotés par les événements est de réaliser que recevoir un événement n'est pas la même chose que mettre à jour l'état de votre application. Ce sont deux responsabilités complètement distinctes.
Le rôle de Slack est simplement de vous dire - « Quelque chose s'est produit. » Tout ce qui vient après relève de votre responsabilité. C'est vous qui décidez si l'événement compte. C'est vous qui décidez à quel objet il appartient. C'est vous qui décidez qui l'a dit. C'est vous qui décidez s'il doit faire partie d'une conversation client.
Tant que votre application n'a pas pris ces décisions, tout ce que vous avez reçu, c'est un autre document JSON. Dans cet article, nous allons enfin boucler la boucle. Les réponses de Slack vont devenir de vrais messages de soutien. Ces messages vont devenir un historique de conversation visible par le client. Et en une fraction de seconde, ils vont apparaître dans le navigateur du client sans rafraîchir la page.
C'est ici qu'un webhook devient une conversation.
Où nous en étions
À la fin de la partie 6, notre architecture ressemblait à peu près à ceci.
Customer
│
▼
Support Conversation
│
▼
Database
│
▼
Slack API
│
▼
Support ChannelLa communication circulait dans un seul sens. Les clients pouvaient parler au soutien. Le soutien pouvait parler à Slack. C'est tout.
Les réponses de Slack arrivaient à notre point de terminaison Events, étaient vérifiées, écrites dans notre table de boîte de réception, puis attendaient patiemment que quelqu'un s'en occupe, ce qui est exactement ce que nous voulions. Rappelez-vous l'un des thèmes récurrents de cette série - Ne faites jamais de vrai travail à l'intérieur d'un webhook. Le webhook existe pour recevoir les événements en toute sécurité. La logique d'affaires se déroule ailleurs.
Et cet ailleurs, c'est le sujet d'aujourd'hui.
Faites connaissance avec slack_event_processor.cfc
Chaque application finit par développer un composant qui fait discrètement tout le travail ennuyeux que personne ne remarque jusqu'à ce qu'il manque. Pour nous, c'est slack_event_processor.cfc. Il a une seule tâche - prendre un événement Slack non traité et déterminer s'il représente quelque chose de significatif dans notre application.
Remarquez ce qu'il ne fait pas. Il ne connaît pas HTTP, ni les webhooks, ni les signatures Slack, ni les nouvelles tentatives. Tout cela a déjà eu lieu. Son univers commence avec une ligne de base de données.
Slack
│
Webhook
│
Inbox Table
│
▼
slack_event_processor.cfcCette séparation s'avère incroyablement précieuse, car le processeur peut maintenant être exécuté à partir de :
- une tâche planifiée
- une file d'attente en arrière-plan
- un outil en ligne de commande
- des tests unitaires
- une logique de reprise
...sans se soucier d'où provient l'événement à l'origine. C'est l'un de ces choix de conception qui ne semble pas important jusqu'à six mois plus tard, quand il vous sauve tout un week-end.
Traitement de la file
Au lieu de demander à Slack d'attendre pendant que nous traitons tout, nous posons périodiquement à notre table de boîte de réception une question beaucoup plus simple - « Avez-vous quelque chose de nouveau pour moi ? »
Conceptuellement, cela ressemble à ceci.
<cfscript>
var events = slackInbox.getPendingEvents();
for (var event in events) {
slackEventProcessor.process(event);
}
</cfscript>L'implémentation réelle est naturellement plus prudente. Les lignes sont verrouillées. Les échecs sont consignés. Les reprises sont limitées. Les événements réussis sont marqués comme terminés. Mais l'idée d'ensemble demeure agréablement ennuyeuse. Et c'est exactement ce que vous voulez du traitement en arrière-plan.
Tous les événements Slack ne méritent pas votre attention
Slack génère un nombre étonnant de types d'événements.
- Messages
- Modifications
- Suppressions
- Réactions emoji
- Épingles
- Retrait des épingles
- Utilisateurs qui rejoignent des canaux
- Utilisateurs qui quittent des canaux
- Indicateurs de saisie
- Probablement le réfrigérateur de quelqu'un qui signale un faible niveau de lait
Votre système de soutien ne se soucie presque d'aucun d'entre eux. L'une des premières sorties dans le processeur ignore simplement tout ce qui n'est pas pertinent.
if (event.type != "message") {
return;
}Cela semble presque trop simple, jusqu'à ce que Slack ajoute cinq nouveaux types d'événements le mois prochain. Ignorer ce qui ne vous intéresse pas est tout aussi important que traiter ce qui vous intéresse. Les processeurs d'événements deviennent considérablement plus faciles à comprendre quand chaque événement non pris en charge se termine immédiatement.
Les robots ne devraient pas avoir de conversations avec eux-mêmes
Le deuxième filtre est tout aussi important. Ignorez les messages des robots. Les robots Slack créent fréquemment des messages. Votre propre application le fait certainement. Si vous ne les filtrez pas, vous finirez par créer une boucle de rétroaction divertissante, mais désastreuse.
Imaginez ceci :
- Votre application publie dans Slack.
- Slack avertit votre application.
- Votre application enregistre le message.
- Peut-être qu'elle le republie.
- Slack reçoit un autre message.
- Slack avertit votre application.
Répétez jusqu'à ce que quelqu'un remarque que l'utilisation du processeur s'approche de l'orbite. C'est l'équivalent moderne de halt and catch fire.
Au lieu de cela, le processeur refuse simplement de traiter le contenu généré par des robots. Conceptuellement :
if (structKeyExists(message, "bot_id")) {
return;
}Ou peut-être :
if (message.subtype == "bot_message") {
return;
}L'implémentation exacte dépend des événements Slack auxquels vous vous êtes abonné, mais le principe ne change jamais. Les humains créent les réponses de soutien. Les bots créent généralement l'infrastructure. Traitez-les différemment.
Un message Slack n'est toujours pas une conversation client
Nous arrivons maintenant à la partie intéressante. Supposons que nous recevions cet événement :
{
"channel": "C123456",
"thread_ts": "1751032712.003900",
"user": "U094832",
"text": "Can you try clearing your browser cache?"
}Pour quel client est-ce? Slack n'en a aucune idée. Il connaît les canaux. Les fils de discussion. Les utilisateurs. Les horodatages.
Votre application connaît les clients.
Ce sont des domaines complètement différents. Heureusement, nous nous y préparons depuis plus tôt dans la série. Lorsque la demande de soutien initiale a été publiée dans Slack, nous avons stocké l'horodatage du fil Slack en même temps que la conversation de soutien. Cet horodatage de fil devient effectivement notre clé étrangère vers l'application.
Slack Thread
│
thread_ts
│
▼
Support ConversationNotre recherche devient donc agréablement simple.
var conversation = supportConversationService.getBySlackThread( event.thread_ts );Si rien ne correspond, ignorez-le. Tous les fils Slack n'appartiennent pas à votre application. Votre équipe de soutien discute probablement des plans de déploiement, du dîner, et de savoir si la production avait vraiment besoin de ce correctif urgent du vendredi après-midi. Ces conversations devraient rester exactement là où elles doivent être.
Résoudre le représentant du soutien
Le client ne se soucie pas que l'utilisateur Slack U094832 ait répondu. Ce qui compte pour lui, c'est que Sarah ait répondu. Ou David. Ou Chris. Les identifiants d'utilisateur Slack font partie de l'infrastructure. Votre application a besoin de vraies personnes. Cela veut dire résoudre les identités Slack vers les représentants du soutien internes.
Slack User
│
▼
Internal User
│
▼
Support MessageC'est un de ces endroits où il est tentant de prendre un raccourci. Vous pourriez penser : « Slack me donne déjà le nom de l'utilisateur. Pourquoi ne pas simplement l'enregistrer? »
Parce que les noms changent.
Les gens se marient. Ils changent leur nom affiché. Ils décident qu'ils s'appellent maintenant « Dave » au lieu de « David ». Quelqu'un se joint à l'équipe avec le même prénom. Slack permet aux utilisateurs de modifier toutes sortes d'informations de profil quand ils veulent, ce qui est très bien pour les humains, mais terrible pour les bases de données.
La seule chose qui ne change pas, c'est l'identifiant d'utilisateur Slack.
Quand quelqu'un autorise votre application Slack, ça vaut la peine de capturer cet identifiant d'utilisateur Slack permanent et de l'associer au représentant du soutien correspondant dans votre propre base de données. À partir de ce moment-là, chaque événement Slack entrant peut être ramené à la bonne personne, peu importe comment elle a décidé de s'appeler cette semaine.
En général, cette recherche est aussi simple que de maintenir une table de correspondance.
var representative = supportRepresentativeService.getBySlackUserId( event.user );Une fois cette recherche réussie, quelque chose d'intéressant se produit. Nous avons fini de penser en termes de Slack. Nous ne nous soucions plus des canaux, des utilisateurs ou des charges utiles d'événement. Nous avons maintenant un représentant du soutien interne, et à partir de ce moment, le reste de l'application fonctionne entièrement selon son propre modèle de domaine.
Si aucun représentant n'existe, vous avez une autre décision à prendre. Rejeter le message? Créer un utilisateur fictif? Consigner une erreur? Chaque application répond à cela différemment.
Personnellement, je préfère échouer bruyamment. Les échecs silencieux sont formidables pour rester inaperçus jusqu'à ce que la direction générale décide de tester le système de soutien.
Persister la réponse
Jusqu'à présent, nous avons travaillé entièrement avec des concepts Slack - utilisateurs, horodatages de fils, charges utiles d'événement et métadonnées de message. Une fois que nous avons identifié la conversation et le représentant du soutien, tout cela disparaît. À partir de là, nous revenons dans notre propre domaine. C'est une transition importante. Le reste de l'application se fiche que la réponse ait pris naissance dans Slack. De son point de vue, un représentant du soutien a répondu à un client.
Une fois que nous avons identifié la conversation, le représentant et le texte du message, le travail restant semble remarquablement ordinaire. Nous insérons simplement un autre message de soutien.
supportMessageService.create(
conversationId = conversation.getId(),
senderType = "support",
senderId = representative.getId(),
message = event.text,
slackTs = event.ts
);Remarquez quelque chose de subtil. Nous ne stockons pas « un message Slack ». Nous stockons une réponse de soutien. Slack a complètement disparu du modèle de domaine. C'est intentionnel. Demain, vous pourriez remplacer Slack. Ou intégrer Microsoft Teams. Ou Discord. Ou des pigeons voyageurs.
Le reste de votre application ne devrait pas le remarquer. Slack n'est qu'un autre moyen de transport. Si vous décidez un jour de remplacer Slack par Teams, Discord ou toute autre plateforme de collaboration à la mode l'an prochain, votre modèle de domaine ne devrait pas avoir à s'en soucier.
La livraison en double se produit
Éventuellement, Slack réessaie un webhook. Peut-être que votre serveur a répondu lentement ou qu'il y a eu un problème réseau transitoire. Peut-être que l'internet a simplement décidé qu'aujourd'hui devait être « intéressant ».
Les événements en double sont tout à fait normaux. Les réponses de soutien en double ne le sont pas. Heureusement, les horodatages Slack sont suffisamment uniques pour faire d'excellentes clés de déduplication.
if ( supportMessageService.existsBySlackTs( event.ts ) ) {
return;
}Une toute petite clause de garde, qui évite potentiellement des milliers de messages en double. N'assumez jamais que les fournisseurs de webhooks livrent exactement une fois. Presque aucun d'entre eux ne le fait.
Dire au navigateur
Passons maintenant à la partie satisfaisante. La base de données contient la réponse, mais le client ne peut toujours pas la voir. Nous avons besoin d'un dernier saut.
Plus tôt dans la série, nous avons présenté les événements envoyés par le serveur (SSE). Au lieu de demander au navigateur d'interroger le serveur toutes les quelques secondes, le serveur annonce simplement quand quelque chose change. Conceptuellement :
Slack
│
▼
Event Processor
│
▼
Database
│
▼
Server-Sent Event
│
▼
BrowserRemarquez que nous n'envoyons pas le message lui-même par la connexion SSE. Nous avisons simplement le navigateur qu'un changement s'est produit. Le navigateur peut ensuite demander l'état le plus récent de la conversation en utilisant les mêmes points de terminaison API qu'il connaît déjà. Cela permet de garder les charges utiles SSE très petites et d'éviter de dupliquer la logique de sérialisation.
Après avoir enregistré la réponse du support, le processeur publie une notification.
sse.publish(
channel = conversation.getId(),
event = {
type = "support_reply",
messageId = message.getId()
}
);Le navigateur écoute déjà. Il reçoit l'événement, récupère le nouveau message et met à jour la conversation. Le client ne rafraîchit jamais la page. Le représentant du support n'ouvre jamais votre application.
Les deux côtés continuent simplement à discuter. C'est à ce moment-là qu'on a enfin l'impression d'être dans une discussion plutôt que dans un courriel avec un meilleur marketing.
Architecture de bout en bout
À ce stade, notre pipeline de support est nettement plus intéressant.
Customer
│
▼
Support Conversation
│
▼
Database
│
▼
Outbound Slack API
│
▼
Slack Support Thread
│
Support Representative
│
▼
Slack Events Webhook
│
Signature Verification
│
▼
Slack Event Inbox
│
▼
slack_event_processor.cfc
│
┌───────────────┼────────────────┐
│ │ │
Ignore Bots Ignore Noise Ignore Unknown Threads
│ │ │
└───────────────┴────────────────┘
│
▼
Resolve Conversation + Representative
│
▼
Persist Support Reply
│
▼
Publish Server-Sent Event
│
▼
Browser UpdatesRemarquez à quel point le webhook fait en réalité peu de choses. Le webhook accepte des données. Tout ce qui est intéressant se produit ensuite. C'est exactement là que vous voulez que la complexité se trouve.
Pourquoi cette architecture vieillit bien
L'un des aspects que j'aime le plus dans cette conception, c'est que chaque étape a une responsabilité unique.
- Le webhook reçoit les événements.
- La boîte de réception stocke les événements.
- Le processeur interprète les événements.
- Le service de support enregistre les réponses.
- Le publieur SSE avise les navigateurs.
Aucune couche n'a besoin de comprendre l'ensemble du flux de travail. Chaque composant n'a besoin que de suffisamment de connaissances pour accomplir sa propre tâche, ce qui réduit considérablement le couplage. Cela rend aussi le débogage remarquablement simple. Si un client signale que les réponses n'apparaissent pas, vous pouvez poser des questions dans l'ordre.
- Est-ce que Slack a livré l'événement ?
- Est-ce que la boîte de réception l'a reçu ?
- Est-ce que le processeur s'est exécuté ?
- Est-ce que la conversation a été trouvée ?
- Est-ce que la réponse a été enregistrée ?
- Est-ce que l'événement SSE a été publié ?
- Où la chaîne s'est-elle arrêtée ?
- Est-ce l'heure du lunch, déjà ?
Vous n'avez pas un énorme « système de support » mystérieux, vous avez plusieurs petits systèmes reliés ensemble. C'est beaucoup plus facile à comprendre.
Ce que nous avons bâti
Au cours des sept derniers articles, nous avons discrètement assemblé quelque chose de bien plus capable qu'une simple intégration de webhook. Nous avons bâti un pipeline de messagerie résilient. Les clients communiquent par votre application. Le personnel de soutien communique par Slack. Les deux côtés restent parfaitement inconscients qu'ils utilisent des systèmes différents. Les messages survivent aux reprises. Le traitement est asynchrone. Les échecs peuvent être repris. Les réponses apparaissent en temps réel.
Plus important encore, chaque message finit par faire partie d'une conversation client durable plutôt que de disparaître à jamais dans l'historique de Slack.
C'est une étape architecturale importante.
Les leçons les plus importantes
S'il y a une idée que j'espère voir retenue après cet article, c'est celle-ci : Les événements sont des observations, pas des objets d'affaires. Slack a observé que quelqu'un a répondu. Votre application a décidé que cette réponse appartenait à une conversation client. Ce sont des responsabilités différentes.
En chemin, nous avons aussi renforcé quelques thèmes qui reviennent tout au long de cette série.
- Les webhooks devraient accuser réception rapidement et reporter le vrai travail.
- File d'attente d'abord. Traitement ensuite.
- Filtrez agressivement.
- Stockez les identifiants du fournisseur, mais ne les laissez pas déborder dans votre modèle de domaine.
- Dédupliquez tout.
- Gardez les transports séparés de la logique d'affaires.
- Poussez les mises à jour vers le navigateur au lieu de forcer les utilisateurs à actualiser.
Aucune de ces leçons n'est propre à Slack. Elles sont utiles pour presque toutes les intégrations pilotées par événements que vous construirez un jour.
La prochaine fois
Notre clavardage de soutien semble maintenant vivant. Les messages passent du navigateur à Slack. Les réponses passent de Slack vers le navigateur. L'ensemble de la conversation reste synchronisé.
Il n'y a qu'un seul problème.
On suppose que tout se passe parfaitement. Les systèmes réels, eux, ne le font pas.
- Les réseaux tombent en panne.
- Slack réessaie.
- Les tâches plantent à mi-parcours du traitement.
- Les travailleurs d'arrière-plan redémarrent.
- Les clients réussissent, d'une manière ou d'une autre, à cliquer sur le même bouton quatre fois en moins d'une seconde.
Dans la partie 8, nous allons rendre l'ensemble du pipeline résilient. Nous allons examiner les reprises, l'idempotence, les messages corrompus, les files d'attente de messages non distribuables, la visibilité opérationnelle et les types de techniques de programmation défensive qui vous permettent de dormir tranquille au lieu de vous demander si un webhook a échoué silencieusement il y a trois heures. Parce que bâtir des intégrations, c'est amusant. Les maintenir en marche, c'est là que la vraie ingénierie commence.
Notre intégration Slack fonctionne. Dans la partie 8, nous allons nous assurer qu'elle continue de fonctionner quand le monde réel finit inévitablement par s'en mêler.