Le WordPress d'aujourd'hui, décodé pour les développeurs

Performance

Sentry a trouvé un N+1 caché derrière une synchronisation HubSpot

Comment une trace Sentry Performance a révélé des centaines de requêtes SQL déclenchées par une synchronisation CRM à chaque sauvegarde d'article.

Par WordPress Développement • 16 octobre 2023 • 5 min de lecture • Aucun commentaire
Sentry a trouvé un N+1 caché derrière une synchronisation HubSpot

Pourquoi une simple sauvegarde d’article prend-elle parfois deux secondes de plus quand un connecteur HubSpot est actif ? C’est la question posée après l’installation de Sentry Performance sur un site éditorial couplé à un CRM, dans le cadre d’une revue de performance de routine plutôt qu’un incident déclaré.

La réponse tenait dans une seule trace, capturée par le SDK Sentry pour PHP configuré avec \Sentry\tracing\SpansSampler activé sur les transactions wp-admin. Ce billet détaille comment lire ce type de trace et corriger la cascade de requêtes qu’elle a mise en évidence.

Ce que montrait la trace Sentry

La transaction correspondant à l’enregistrement d’un article listait une opération dominante intitulée db.query, répétée non pas deux ou trois fois, mais 340 fois dans une seule sauvegarde. Chaque span portait la même requête paramétrée, à quelques valeurs près :

SELECT meta_value FROM wp_postmeta
WHERE post_id = %d AND meta_key = 'hubspot_contact_id'

Le nombre de spans identiques correspondait presque exactement au nombre de contacts associés à l’article dans une taxonomie personnalisée « intervenants », utilisée par la rédaction pour lier chaque article aux experts cités. Le connecteur HubSpot, à chaque sauvegarde, parcourait la liste des intervenants pour vérifier leur identifiant CRM et déclencher une mise à jour de propriété côté HubSpot.

Le hook en cause

L'essentiel à retenir : Une trace de performance regroupe les requêtes par origine, pas seulement par durée ; Le hook save_post est un terrain propice aux appels en cascade ; Un simple cache statique en mémoire a suffi à éliminer le problème

Le code fautif était accroché au hook save_post, exécuté à chaque sauvegarde y compris les sauvegardes automatiques (wp_is_post_revision() non vérifié) :

add_action( 'save_post', function ( $post_id ) {
    $intervenants = wp_get_post_terms( $post_id, 'intervenant' );
    foreach ( $intervenants as $terme ) {
        $contact_id = get_term_meta( $terme->term_id, 'hubspot_contact_id', true );
        if ( $contact_id ) {
            hubspot_sync_contact( $contact_id );
        }
    }
});

À première vue, rien de choquant : une boucle sur une poignée de termes, un appel à get_term_meta() par itération. Le problème n’était pas visible dans le code lui-même mais dans son contexte d’exécution : la fonction hubspot_sync_contact() effectuait elle-même une lecture supplémentaire en base pour récupérer les articles liés à ce contact, afin de calculer un score d’engagement — d’où la multiplication des requêtes bien au-delà du nombre d’intervenants.

Pourquoi Query Monitor n’avait rien détecté

Le plugin Query Monitor, déjà installé sur l’environnement de recadrage, affichait bien un nombre de requêtes élevé sur cette action, mais noyé dans la liste générale des centaines de requêtes qu’une sauvegarde d’article déclenche de toute façon (métadonnées, révisions, taxonomies, cache de transients). Sans le regroupement par origine et par durée cumulée qu’offre une trace de performance dédiée, l’anomalie restait statistiquement invisible au milieu du bruit.

C’est la vraie valeur ajoutée d’un outil comme Sentry Performance sur ce type de diagnostic : il agrège les spans par empreinte de requête et affiche un temps cumulé par origine, ce qui fait immédiatement ressortir un pattern répétitif que l’œil humain aurait mis longtemps à repérer dans un flux de logs bruts.

Le correctif appliqué

Deux niveaux de correction ont été retenus. D’abord, un cache statique en mémoire pour la durée de la requête HTTP, évitant de relire deux fois le même identifiant de contact :

function get_hubspot_contact_id_cache( $terme_id ) {
    static $cache = [];
    if ( ! array_key_exists( $terme_id, $cache ) ) {
        $cache[ $terme_id ] = get_term_meta( $terme_id, 'hubspot_contact_id', true );
    }
    return $cache[ $terme_id ];
}

Ensuite, et surtout, la synchronisation elle-même a été déplacée hors du cycle de requête grâce à Action Scheduler (as_enqueue_async_action( 'hubspot_sync_contact', [ $contact_id ] )), ce qui a rendu la sauvegarde d’article instantanée du point de vue de la rédaction, la synchronisation CRM s’exécutant en tâche de fond quelques secondes plus tard.

Le gain mesuré après correctif

  • Le nombre de requêtes SQL par sauvegarde d’article est passé de 340 à 4, la boucle sur les intervenants ne déclenchant plus de lecture redondante.
  • Le temps de sauvegarde perçu côté rédaction est passé de « environ trois secondes » à un enregistrement quasi immédiat, la synchronisation HubSpot n’étant plus sur le chemin critique.
  • La charge sur le connecteur HubSpot lui-même a diminué, les appels étant désormais espacés par la file d’Action Scheduler plutôt que déclenchés en rafale.

Une trace de performance qui regroupe par empreinte de requête révèle en quelques minutes ce qu’un audit manuel de code n’aurait peut-être jamais mis au jour.

En résumé

Le N+1 n’est pas toujours une boucle évidente dans le code qui l’a déclenché : ici, la boucle apparente portait sur trois ou quatre intervenants, la vraie explosion se cachait dans une fonction appelée en aval. Sentry Performance a permis de voir le symptôme (340 requêtes identiques) avant même de comprendre la cause exacte, ce qui a orienté directement l’investigation vers le bon fichier. Sans cet outil, le correctif aurait probablement ciblé la mauvaise boucle.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi