Retour au Centre de connaissances
Empreinte

ua-parser-js : Client Hints, bots et un plafond de 500

Exécutez le bundle épinglé de ua-parser-js sur des user agents et Client Hints fixes : l’identité rapportée, les marqueurs ignorés et l’effet d’un plafond de 500 sur un jeton.

BotBrowser Team

Documentation

Vous voulez la documentation structurée pour Empreinte ?

Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.

BotBrowser prend en charge le rejeu d’un fixture autorisé de même origine sous un profil déclaré et la conservation d’observations bornées sur ce qu’une page rapporte, mais il ne peut pas faire concorder un analyseur tiers avec le navigateur qu’il mesure, changer le plafond de 500 caractères d’une bibliothèque, ni transformer une étiquette analysée en verdict sur qui est de l’autre côté. Cette revue épingle l’arbre public de ua-parser-js au commit bd7480198cd0d237b43a24a964f424727abb78e9 (version 2.0.10 du paquet), exécute son analyseur empaqueté sur une liste fixe de chaînes de user agent et d’ensembles d’en-têtes Client Hints, et rapporte ce que le code a réellement renvoyé.

TL;DR

  • L’analyseur épinglé étiquette un user agent headless en Chrome Headless et son propre classificateur de bots répond encore isBot=false pour la même chaîne.
  • Un user agent d’application Electron est rapporté comme Chrome simple ; le nom de navigateur de l’analyseur n’y porte aucun signal d’automatisation.
  • Fournir une chaîne de user agent fait ignorer à l’analyseur une liste de marques sec-ch-ua contradictoire, donc Client Hints ne supplante pas ce que la chaîne disait déjà.
  • L’entrée est tronquée à 500 caractères, et un jeton de bot qui commence à l’index 500 est invisible au classificateur tandis qu’un jeton commençant à 490 est trouvé.
  • Tout cela décrit un commit et un bundle ; ce n’est pas une affirmation sur la façon dont un site note une session.

Contents

UA_INPUT tronque à 500 caractères vers PARSE, puis les branches BROWSER_NAME, IS_BOT et CH_IGNORED

Ce que le paquet épinglé contient vraiment

La version 2.0.10 répartit src/ en sept répertoires. src/main garde l’analyseur, src/bot-detection garde le classificateur, src/browser-detection et src/device-detection gardent des comparateurs séparés, src/extensions garde les listes de bots, de crawlers et de fetchers, src/enums garde les vocabulaires et src/helpers garde les petites utilitaires. Le bundle publié dist/ua-parser.pack.js exporte UAParser, VERSION et cinq énumérations (BROWSER, CPU, DEVICE, OS, ENGINE) ; le sous-module de bots est une entrée séparée qui exporte isBot, isAIAssistant et isAICrawler.

Les deux moitiés lisent la même chaîne par des chemins de code différents, et cette différence est le sujet de cette revue. UAParser applique des expressions régulières au texte du user agent et, quand il reçoit un objet d’en-têtes, aux en-têtes Client Hints aussi. isBot fait quelque chose de plus étroit : il analyse le user agent via le registre d’extensions puis demande si le type de navigateur obtenu est cli, crawler, fetcher ou library. Une bibliothèque qui produit un nom de navigateur plausible peut donc coexister avec un classificateur qui dit que la chaîne n’est pas un bot, et les deux résultats répondent à des questions différentes.

Deux constantes dans src/main/ua-parser.js décident de la quantité de texte que chaque moitié peut voir. L’analyseur déclare UA_MAX_LENGTH = 500, et setUA applique trim(ua, UA_MAX_LENGTH) avant toute correspondance. Tout ce qui suit, y compris la chaîne que le classificateur de bots réanalyse, travaille donc sur au plus les 500 premiers caractères de ce qui a été fourni.

Exécuter l’analyseur sur des chaînes de user agent fixes

La sonde charge le bundle publié et le sous-module de bots épinglé, puis imprime une ligne par entrée. Elle lit une liste fixe ; elle ne charge aucune page, n’ouvre aucun profil et ne contacte aucun service :

node /tmp/uap-full-probe.mjs

La sortie observée, section par section :

section: user-agent parse
chrome-win: browser=Chrome/155.0.0.0 engine=Blink os=Windows device=desktop
safari-mac: browser=Safari/18.4 engine=WebKit os=macOS device=desktop
headless-linux: browser=Chrome Headless/155.0.0.0 engine=Blink os=Linux device=desktop
edge-win: browser=Edge/155.0.0.0 engine=Blink os=Windows device=desktop
electron-win: browser=Chrome/155.0.0.0 engine=Blink os=Windows device=desktop
googlebot: browser=undefined/undefined engine=undefined os=undefined device=desktop
iphone-safari: browser=Mobile Safari/18.4 engine=WebKit os=iOS device=mobile

Trois détails de ce bloc comptent plus que le reste. La ligne headless-linux rapporte un nom de navigateur distinct, Chrome Headless, ce qui signifie que l’analyseur porte bien une étiquette d’allure automatisation pour cette chaîne. La ligne electron-win rapporte Chrome simple, donc une application construite sur Electron est indistinguable d’un navigateur de bureau dans ce champ. Et googlebot rapporte undefined pour navigateur, moteur et système d’exploitation tout en rapportant encore device=desktop, ce qui montre que « aucune correspondance » s’exprime comme des champs absents et non comme une erreur.

Le champ device mérite sa propre note car il est facile de le lire comme une affirmation plus forte qu’il n’est. googlebot et chrome-win reviennent tous deux en desktop, et headless-linux aussi ; le champ est un panier de repli, pas une mesure. Qui lit device=desktop comme « c’est vraiment une machine de bureau » a lu une valeur par défaut comme une preuve.

Où le classificateur de bots contredit le nom de navigateur

La deuxième section de la même exécution interroge le classificateur sur toutes les chaînes ; le bloc ci-dessous est un extrait de la sortie complète consignée dans le fichier de preuve :

section: bot classifier
chrome-win: isBot=false isAIAssistant=false isAICrawler=false
headless-linux: isBot=false isAIAssistant=false isAICrawler=false
electron-win: isBot=false isAIAssistant=false isAICrawler=false
googlebot: isBot=true isAIAssistant=false isAICrawler=false

La ligne headless-linux est l’intéressante. L’analyseur appelle cette chaîne Chrome Headless, et le classificateur l’appelle pas un bot. Les deux sont des affirmations correctes sur leur propre jeu de règles : le classificateur ne consulte pas le nom de navigateur pour HeadlessChrome ; il consulte le type de navigateur, et une compilation Chromium headless déclare encore le type de navigateur ordinaire. Un rapport qui cite l’une de ces deux réponses et sous-entend l’autre rapporte mal la bibliothèque.

La ligne electron-win répète le motif dans l’autre sens. L’analyseur ne donne aucune étiquette d’automatisation, et le classificateur s’accorde à dire qu’il n’y a pas de bot. Une application Electron est donc invisible aux deux champs sauf si une autre partie de la pile regarde le jeton Electron/ directement, ce que ce bundle ne fait dans aucun des deux chemins que la sonde a exercés.

La ligne googlebot montre le résultat le plus fort du classificateur, isBot=true, tandis que l’analyseur ne renvoie rien. C’est le cas inverse : la liste de crawlers reconnaît la chaîne bien que le comparateur générique de navigateurs n’ait aucun motif pour elle. Lire seulement browser.name ferait paraître cette ligne comme un échec d’analyse plutôt qu’une correspondance de bot.

Par contraste, les listes orientées IA se comportent différemment de la liste générique de bots. Dans la sonde plus étroite le classificateur a répondu isAICrawler=true pour GPTBot et ClaudeBot, et isAIAssistant=true pour ChatGPT-User et Claude-User, avec isBot=true pour les quatre. Une chaîne HeadlessChrome simple et une chaîne Electron sont restées false sur les trois prédicats, ce qui concorde avec le tableau ci-dessus.

Où Client Hints cesse de compter

La troisième section fournit une liste de marques de style Chromium et des en-têtes de plateforme, puis ne change que la chaîne de user agent :

section: client hints
hints-only: browser=undefined/undefined os=undefined device=desktop
ua-safari-plus-chrome-hints: browser=Safari/18.4 os=macOS device=desktop

La première ligne est celle qu’on attend : sans user agent et avec seulement des en-têtes sec-ch-ua, l’analyseur ne renvoie rien dans ce chemin d’appel. Les en-têtes seuls ne suffisent pas ici à produire un nom de navigateur, car la liste de marques est consommée par l’étape d’analyse Client Hints et non par le comparateur générique, et la sonde a appelé le résultat simple.

La seconde ligne est la surprise. La chaîne de user agent dit Safari, les en-têtes disent Chromium et Google Chrome sous Windows, et le résultat est Safari/18.4 sous macOS. Dans ce chemin la chaîne l’emporte et la liste de marques contradictoire est ignorée, donc une page dont le user agent et les Client Hints se contredisent est rapportée comme ce que la chaîne affirmait. C’est une propriété utile à connaître avant de traiter une étiquette d’analyseur comme un recoupement : un désaccord que vous attendiez à voir détecté peut passer tout droit.

La raison pour laquelle la seconde ligne conserve la chaîne est visible dans parseCH(). Quand une liste de marques est présente, la boucle parcourt chaque marque, ignore tout nom correspondant à /not.a.brand/i, et ne remplace le nom courant que sous une condition étroite : le nom précédent doit commencer par Chrom, ou le nom précédent doit être Edge et la nouvelle marque doit être WebView2. Avec un nom précédent Safari, aucune ne tient, donc toutes les marques sont examinées et aucune ne remplace le nom analysé. L’entrée Chromium ne sert qu’à la version du moteur quand le type d’élément est ENGINE, pas au nom de navigateur.

getBrowser().withClientHints() existe et consulte bien les en-têtes, et il renvoie un thenable plutôt qu’un objet simple. La sonde a confirmé cette forme, et confirmé aussi que l’attendre sur les entrées contradictoires renvoyait encore Safari/18.4, car la même règle de réconciliation conserve le nom non Chromium. La mise en garde importante pour un lecteur est qu’un getResult() synchrone et un withClientHints() attendu sont des formes d’appel différentes avec des réponses différentes, et un rapport devrait dire laquelle a produit l’étiquette.

Une étiquette d’analyseur n’est qu’une lecture de la même session. Quand la question est ce qu’un détecteur fait des noms d’allure automatisation que cette exécution a produits, la revue du registre de détecteurs BotD exécute un autre détecteur public sur des états explicites, et détection et prévention de l’automatisation recense l’ensemble plus large de surfaces qu’une page peut inspecter. Les lire ensemble évite qu’une seule bibliothèque tienne lieu de toute la pile.

Où le plafond de 500 caractères cache un jeton

La dernière section teste la constante nommée dans src/main/ua-parser.js :

section: marker position vs 500-char cap
pad=430: totalLength=500 markerStart=490 isBot=true
pad=440: totalLength=510 markerStart=500 isBot=false

Deux chaînes ne diffèrent que par la quantité de remplissage avant un jeton GPTBot/1.2. À une longueur totale de 500 avec le jeton commençant à l’index 490, le classificateur le trouve. À 510 caractères avec le jeton commençant à l’index 500, le classificateur ne le trouve pas. La frontière est exactement là où trim(ua, UA_MAX_LENGTH) coupe la chaîne, donc le jeton n’est pas mal analysé ; il n’est jamais vu.

C’est un mode de défaillance concret à la forme claire : une page peut être reconnue comme bot quand son jeton tombe dans les 500 premiers caractères et passer silencieusement quand le même jeton est poussé juste au-delà de la frontière par un long commentaire ou une liste d’extensions. Rien du jeton n’a changé, seulement son décalage. Un test qui ajoute un marqueur à un user agent long réaliste puis rapporte « non détecté » mesure le plafond, pas le jeton.

L’atténuation n’est pas d’élever la constante dans une copie de la bibliothèque, car le comportement épinglé est celui contre lequel le reste du code de la pile a été écrit. C’est de garder l’observation honnête : consignez la longueur de l’entrée à côté du verdict, et quand un marqueur est attendu, confirmez que son décalage tombe dans le plafond. Un jeton de 10 caractères à l’index 500 et le même jeton à l’index 490 sont le même contenu de chaîne et une preuve différente.

Quelles listes de règles alimentent le classificateur

Le classificateur n’est pas une liste ; c’est trois enveloppes sur trois registres. BotTypes lit le type de navigateur produit par l’extension Bots et traite cli, crawler, fetcher et library comme types de bot. AIAssistants nomme des entrées précises du registre Fetchers, et AICrawlers nomme des entrées précises du registre Crawlers. Les listes sont longues d’une manière qui compte : le seul tableau AICrawlers épinglé nomme 53 entrées de crawler distinctes de fournisseurs aussi divers qu’Anthropic, ByteDance, Cloudflare, Hugging Face, Meta, OpenAI, Semrush et Zhipu, et le tableau AIAssistants nomme 8 entrées, une pour Amazon, Anthropic, Cohere, DuckDuckGo, Google, Mistral, OpenAI et Perplexity.

Cette structure explique pourquoi la même chaîne peut être isBot=true et isAICrawler=false. isBot demande seulement si le type analysé est un type de bot, tandis que isAICrawler demande si le nom de navigateur analysé correspond à l’un de 53 noms énumérés. Un crawler qui correspond à l’extension Bots par type mais est absent de la liste énumérée de crawlers IA est un bot et n’est pas un crawler IA. Aucune des deux réponses n’est fausse ; ce sont deux requêtes sur deux ensembles différents.

Les listes vieillissent aussi. Un nom présent aujourd’hui dans le tableau épinglé de 53 entrées peut être retiré ou renommé dans une version ultérieure, et un nouveau crawler apparu après ce commit est invisible à tous les prédicats par construction. C’est la même raison pour laquelle la revue épingle un commit plutôt qu’une branche : la réponse est une propriété de la révision de la liste autant que de la chaîne d’entrée.

Ce qu’un test de mutation montrerait ou non

Un prochain pas tentant est de changer un caractère d’un user agent et d’affirmer que l’analyseur est fragile. La sonde suggère déjà pourquoi ce serait une affirmation faible. Le jeton Electron/33.0.0 était présent dans la chaîne et n’a rien changé, parce qu’aucun des deux chemins que la sonde a exercés ne le cherche ; une mutation qui retirerait le jeton ne changerait rien non plus, et le test ressemblerait à une démonstration de robustesse plutôt qu’à un champ inutilisé.

L’inverse est plus instructif. Ajouter un jeton GPTBot/1.2 à une chaîne Chrome a fait passer isBot de false à true dans la sonde plus étroite, donc une mutation sur ce jeton démontre que le classificateur le lit. Tester le même jeton à deux décalages, 490 et 500, isole alors le plafond plutôt que le jeton. Une seule mutation ne peut pas séparer « l’analyseur ignore ce texte » de « l’analyseur n’a jamais vu ce texte », et seule une paire de décalages le peut.

La même logique s’applique aux en-têtes. Une exécution qui ajoute une liste de marques sec-ch-ua contradictoire à un user agent Safari et n’observe aucun changement est une preuve que la chaîne a pris le pas dans ce chemin d’appel. Une exécution qui retire complètement le user agent et observe undefined est une preuve que les en-têtes seuls ne sont pas consommés par l’appel de résultat simple. Deux exécutions, deux affirmations différentes ; une seule exécution les aurait confondues.

Lire une identité analysée sans surestimer

Le résultat d’un analyseur soutient une affirmation étroite : ce commit, avec cette chaîne et ces en-têtes, a produit cette étiquette. Il ne soutient pas une affirmation que la session est un navigateur, un bot ou une personne, ni qu’un autre site arrivera à la même conclusion, car un autre site peut épingler une autre version, passer d’autres en-têtes ou poser une autre question.

Gardez quatre champs ensemble et le relevé reste utilisable. Le commit, car les listes de motifs comme le plafond sont modifiés entre versions. L’entrée, littérale et avec sa longueur, car le plafond fait du décalage une partie de l’entrée. La forme d’appel, car getResult() et withClientHints() peuvent diverger. Et le prédicat, car isBot, isAIAssistant et isAICrawler sont trois réponses séparées que la sonde a montrées se divisant sur la même famille de chaînes.

La page de documentation sur les propriétés de navigator décrit comment BotBrowser garde navigator.platform, navigator.userAgentData et les valeurs associées alignés sur le profil chargé. Cette revue ajoute le côté lecture : ce qu’un analyseur très utilisé fait des chaînes que ces valeurs produisent, où son nom de navigateur et son classificateur de bots divergent, et où son propre plafond d’entrée rend un marqueur invisible. La cohérence des valeurs rapportées et l’exactitude d’une analyse tierce sont des questions distinctes.

Pour toute décision sur une session réelle, utilisez un résultat d’intégration autorisé du système qui décide plutôt que d’extrapoler depuis cette revue de code. Une étiquette analysée consignée dans un fixture et un verdict de production sont deux types de preuve différents, et c’est dans l’écart entre les deux que se produit la surestimation.

Practical conclusion

Épinglez le commit, exécutez l’analyseur sur des entrées fixes et consignez la longueur, la forme d’appel et le prédicat précis à côté de chaque étiquette. Dans ce commit un user agent headless est nommé Chrome Headless tandis que le classificateur de bots répond false, une chaîne Electron est nommée Chrome, une liste de marques Client Hints contradictoire est ignorée quand une chaîne de user agent est présente, et un jeton qui commence à l’index 500 est invisible tandis que le même jeton à l’index 490 est trouvé.

Sources

Next reading

Consultez fingerprinting par Client Hints pour la façon dont l’ensemble d’en-têtes varie, et réduction du user agent et migration vers Client Hints pour ce que les navigateurs ont retiré de la chaîne.

#Ua-Parser-Js#user agent#client hints#Détection De Bots#Provenance De L’analyse

Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?

Faites passer BotBrowser de la recherche à la production

Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.