🍪 🔄 Synchronisez votre CRM depuis Rails avec Etlify
Ce mois-ci, nous parlons synchronisations CRM depuis Rails, temps de lecture : 8 minutes
🚨Cette édition est relativement longue, votre boîte mail risque de la tronquer. Je vous conseille de cliquer sur le titre ci-dessus pour l’ouvrir dans votre navigateur web. 🚨
Hello les petits Biscuits !
Bienvenue sur la 44ème édition de Ruby Biscuit.
Vous êtes maintenant 612 abonnés 🥳
Vous lisez Ruby Biscuit, la newsletter Rails de Capsens. S’abonner gratuitement
Parenthèse sur notre virage IA
Chez Capsens, on n’écrit plus vraiment de code nous-mêmes. C’est Claude qui code, nous on fait de l’archi, on spec, on relit, on teste, on valide. Le résultat : environ x1,5 de productivité sur les développements qu’on réalise pour nos clients.
Ce n’est pas encore le mythique x3. On héberge des plateformes avec de gros flux financiers et des documents d’identité, la maintenabilité à long terme et la sécurité ne sont pas négociables pour nous. La relecture humaine et les tests ont un coût et on le sait. On travaille activement pour gagner en productivité sur ces sujets afin qu’ils ne limitent pas les gains de productivité. Notre nouveau directeur technique est à fond là-dessus.
On propose aujourd’hui cet accompagnement à d’autres équipes tech. Si vous cherchez à prendre ce virage avec une équipe qui l’a fait concrètement, on serait ravis d’en discuter.
Bonne lecture.
Chez l’un de nos clients qui propose une plateforme d’investissements en ligne, l’équipe commerciale travaille depuis Airtable. C’est leur outil au quotidien pour suivre les utilisateurs, les investissements et les mouvements financiers. Sauf que la donnée source, elle, vit dans l’app Rails. Il faut donc pousser les données de Rails vers Airtable et les garder à jour.
Au début, nous synchronisions un model. Puis deux. Puis cinq. Chaque model avait son propre worker, son serializer, sa transaction, ses specs. Six à huit fichiers à créer pour chaque nouveau model synchronisé. Vingt-quatre fichiers dédiés rien qu’à la sync. Et quand ça cassait, la synchronisation périodique repassait sans corriger le problème et les erreurs polluaient Appsignal, notre outil de monitoring.
Le vrai problème n’était pas le code existant. C’était la question : “combien de temps pour ajouter un sixième model ?” Réponse : une demi-journée, à câbler les callbacks, copier-coller la logique de digest, prier pour ne rien oublier.
Nous avons d’abord cherché des gems existantes. La plupart ciblaient un CRM spécifique (HubSpot, Salesforce) ou imposaient un couplage fort avec ActiveRecord. Rien qui colle à notre besoin : un système déclaratif, agnostique du CRM, capable de gérer les dépendances entre models.
Nous avons décidé d’écrire notre propre gem. L’idée de départ : rendre la sync CRM aussi simple qu’un has_many ou un validates. Déclarer dans le model ce que nous synchronisons, comment nous le transformons, et laisser la gem gérer le reste.
Quelques mois plus tard, nous avons Etlify, une gem Rails open source créée par Capsens. Le nom vient de l’acronyme ETL : Extract, Transform, Load. C’est exactement ce que fait la gem : extraire les données d’ActiveRecord, les transformer via un serializer, et les charger dans le CRM.
Ce que nous y avons gagné
Avant d’entrer dans le code, voici les chiffres mesurés sur notre migration :
L’architecture en 30 secondes
Etlify repose sur quatre briques, qui suivent la logique ETL.
Extract : la détection des stale records scanne périodiquement vos models pour détecter les records dont le digest a changé sans appel explicite, et relance leur synchronisation.
Transform : le serializer (appelé dictionary dans la gem) transforme un record ActiveRecord en Hash CRM-compatible. Un par model synchronisé.
Load : le synchronizer orchestre le chargement. Il calcule une empreinte SHA256 du payload. Si rien n’a changé, il passe. Sinon il appelle l’adapter (la couche HTTP vers le CRM) et stocke le résultat dans crm_synchronisations.
crm_sync! → Worker (async) → Synchronizer
├── sync_if → false ? → :skipped
├── dependency manquante ? → PendingSync → :buffered
├── digest identique ? → :not_modified
└── Serializer#to_h → Adapter#upsert! → :syncedLe worker, l’adapter et le synchronizer sont partagés entre tous les models. Vous n’écrivez que ce qui est spécifique : le serializer et la config.
Implémentation
Installation
Ajoutez la gem à votre Gemfile, avec faraday (HTTP), sidekiq-throttled (rate limiting) et sidekiq-unique-jobs (dédup des jobs) si ce n’est pas déjà fait :
gem "etlify", git: "git@github.com:CapSens/etlify.git", tag: "v0.9.3"Puis :
bundle installAvant de lancer les migrations, créez l’initializer pour que la gem soit configurée :
Ensuite, générez et lancez les migrations :
rails g etlify:migration create_crm_synchronisations
rails g etlify:migration create_etlify_pending_syncs
rails db:migratecrm_synchronisations stocke pour chaque record synchronisé :
etlify_pending_syncs garde les syncs bloquées par une dépendance (nous y reviendrons).
L’adapter
La gem fournit le contrat. L’adapter, c’est vous qui l’écrivez. Voici le nôtre pour Airtable :
upsert! retourne le crm_id. delete! retourne un booléen. Les méthodes CRUD privées sont du Faraday classique (POST pour créer, PATCH pour mettre à jour, GET avec filterByFormula pour chercher un record existant).
Point important : la gem ne déclenche pas delete! sur after_destroy. Si vous supprimez un record en base, le record côté CRM reste. À vous de décider où et quand appeler delete! explicitement.
Pour un autre CRM, implémentez upsert!, delete! et le mapping d’erreurs dans handle_response. Le reste change, la mécanique reste la même.
La config YAML
Chaque model a son fichier YAML. L’idée c’est de découpler vos noms de champs des IDs Airtable. Un champ renommé côté CRM ? Vous changez le YAML, pas le code.
Déclarer un model synchronisable
Deux choses à ajouter au model : include Etlify::Model pour le DSL, et has_many :crm_synchronisations pour l’association polymorphique.
Le YAML est chargé au boot via la constante CONFIG_PATH du serializer, un seul endroit où le chemin est défini. crm_object_type reçoit le table ID. id_property sert à retrouver un record existant côté CRM.
Quatre options dans le DSL méritent que nous nous y arrêtions. Nous avons mis un moment à bien les cerner.
sync_dependencies: [:customer] est bloquant. Si le Customer n’a pas de crm_id, la sync est mise en attente. Un PendingSync est créé. Etlify déclenche la sync du Customer en cascade. Quand celui-ci sera sync, les syncs en attente seront exécutées.
dependencies: [:products] est non bloquant. Le serializer de l’Order inclut des données des Products (ex : leur nom, leur prix). Si un Product change, le digest SHA256 de l’Order change aussi au prochain calcul. Le cron de stale records détecte cette différence et re-sync l’Order automatiquement.
sync_if filtre les records éligibles. Si le lambda retourne false, le synchronizer retourne :skipped et ne touche pas au CRM. Attention : un record déjà synchronisé dont le sync_if retourne ensuite false ne sera ni re-sync ni supprimé côté CRM. Cela signifie que si un record change d’état (par exemple, il redevient non éligible), il restera tel quel côté CRM. Pour le retirer, appelez delete! explicitement. À utiliser avec discernement, sur des états réellement définitifs.
stale_scope restreint le scan du cron aux records concernés.
La cascade fonctionne en profondeur. Imaginez : Order → Customer → Company. Etlify met en attente et remonte la chaîne jusqu’à trouver un crm_id. Si ça vous rappelle les poupées russes, c’est normal.
Le serializer
Le serializer, c’est le fichier où vous décidez ce que le CRM voit de vos données. D’abord, la classe de base :
Le helpers/h donne accès aux helpers Rails (h.number_to_currency, h.truncate, etc.) directement dans vos serializers. Ça dépanne plus souvent qu’on ne le pense.
Puis le serializer du model :
Les clés du Hash retourné par to_h sont les field IDs Airtable, pas vos noms de colonnes. Un serializer par model synchronisé.
Le worker
Le model sait quoi, le serializer sait comment. Reste le transport. La gem fournit un Etlify::SyncJob par défaut. Ici nous le remplaçons par un worker Sidekiq avec throttling pour respecter les limites Airtable :
Le throttle :airtable_api doit être déclaré dans votre initializer Sidekiq :
Et la queue crm dans votre config :
Le worker a retry: false côté Sidekiq. Le synchronizer fait un seul essai par invocation : s’il échoue, il incrémente error_count dans crm_synchronisations et passe au suivant. Pas de retry en boucle, c’est le cron des stale records qui rattrape le coup au cycle suivant. Après 3 échecs consécutifs (configurable via max_sync_errors), le record est exclu du cron automatique. Il reste synchronisable manuellement via crm_sync!. Un appel réussi remet error_count à zéro.
En complément, un worker cron rattrape les records dont le digest a changé sans appel explicite :
Planifiez-le dans votre config/schedule.yml à la fréquence qui vous convient.
Déclencher la sync
Tout est en place. Pour synchroniser un record : order.crm_sync!(crm_name: :airtable). Un one-liner depuis vos services, transactions ou controllers. Nous l’appelons après un paiement validé, dans nos transactions de membership, dans les services d’onboarding. Le synchronizer gère le reste.
Migrer depuis un système legacy
Si vos models avaient une colonne airtable_id, ajoutez un fallback :
Les deux systèmes cohabitent. Vous migrez model par model.
Pour éviter de re-sync tous vos records via l’API, un rake task crée les CrmSynchronisation en masse à partir des airtable_id existants :
Le task stocke l’empreinte actuelle. Seuls les records modifiés après le backfill seront re-sync. Pas de flood API.
Tests
Pour vos tests, mockez les appels HTTP vers Airtable plutôt que de remplacer l’adapter. Cela permet de tester le vrai comportement de bout en bout, y compris votre handle_response et le mapping d’erreurs :
Puis testez vos serializers et le comportement de sync :
Etlify n’est pas limitée à Airtable. L’architecture par adapter permet de brancher n’importe quel CRM. La gem embarque un NullAdapter pour vos tests et le développement local. Si vous utilisez ActiveAdmin, les tables crm_synchronisations et etlify_pending_syncs se branchent très bien pour monitorer vos syncs et relancer les records en erreur.
Si vous synchronisez un CRM depuis Rails et que ça commence à devenir pénible, essayez-la. Nous aurions aimé l’avoir plus tôt.
La gem est open source : github.com/CapSens/etlify
— Benjamin, développeur chez Capsens
















