Requêtes média CSS dans CreepJS : deux canaux, une préférence
Analyse du collecteur CSS média figé de CreepJS : une préférence lue par une propriété @media et par matchMedia, ses quatorze clés et le balayage de la plage d’écran.
BotBrowser Team
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.
Les préférences d’affichage font partie des rares surfaces qu’une page peut lire sans demander d’autorisation. Un collecteur peut lire chaque préférence deux fois : il laisse d’abord le moteur appliquer une requête média puis lit la propriété personnalisée obtenue ; ensuite, il demande à matchMedia si la requête correspond. Les deux réponses viennent du même moteur, mais pas du même chemin de code. Si le collecteur enregistre les deux, il conserve deux observations indépendantes d’une préférence. Cette page suit cette séparation dans un collecteur open source épinglé sur une révision précise, nomme les quatorze clés qu’il expose par canal, explique la branche qui affiche une note de blocage plutôt qu’un objet vide et décrit le balayage qui récupère les dimensions de l’appareil au moyen de requêtes média plutôt que de screen. Rien ici n’est un score, une note ou une prédiction de la façon dont un site classe une session.
BotBrowser configure les paramètres d’affichage du profil dans le cœur du navigateur. Il ne contrôle pas directement la conclusion d’un collecteur tiers et ne garantit pas un résultat de détection.
TL;DR
- Le collecteur écrit trente règles
@mediadans un élémentstyle, puis relit quatorze propriétés personnalisées avecgetComputedStyle. Chaque valeur arrive donc par la cascade plutôt que par un appel d’API. - Il pose ensuite les mêmes questions avec
matchMediaet enregistre un second objet contenant les mêmes quatorze clés. Chaque préférence possède alors deux réponses enregistrées. - Deux clés interrogent volontairement le
matchMediade la fenêtre principale, et non celui du cadre, car un cadre peut hériter d’un schéma de couleurs différent de celui de la fenêtre qui le contient. - Si les quatorze clés d’un canal sont toutes fausses ou absentes, une note de blocage s’affiche. Une clé sans correspondance est enregistrée comme
undefined; ce n’est pas la même chose. - La largeur et la hauteur de l’appareil sont récupérées en testant
device-widthetdevice-heightavec un candidat, puis dix plages de 1000 pixels. Il s’agit d’une mesure par requêtes média, pas d’une lecture descreen.width.
Sommaire
- Ce que lisent les deux canaux
- Ce qu’une réponse enregistrée établit ou non
- Le balayage de la plage d’écran
- Un relevé jumelé pour une exécution
- Conclusion pratique
Ce que lisent les deux canaux
Les deux canaux répondent aux mêmes quatorze questions. La différence tient à l’endroit où la réponse est calculée.
Le canal des propriétés personnalisées
Le premier canal n’appelle aucune fonction de correspondance. Il place dans le corps du document un élément style contenant trente règles média. Chaque règle définit une propriété personnalisée sous une condition, puis le collecteur lit le style calculé du corps et en extrait quatorze propriétés :
body.innerHTML = `<style>@media (prefers-color-scheme: light) {body {--prefers-color-scheme: light}}</style>`;
const style = getComputedStyle(body);
style.getPropertyValue('--prefers-color-scheme').trim() || undefined;
Deux propriétés de cette méthode comptent lors de la lecture des résultats. Le moteur de styles produit la valeur en appliquant une règle conditionnelle. Si une clé ne correspond pas, la propriété reste absente et l’expression renvoie undefined, plutôt qu’une chaîne vide ou false. De plus, les règles se trouvent dans le document : la réponse correspond à ce que le moteur a décidé pour les conditions de ce document, et non à une valeur calculée par un script à partir d’autres lectures. Le module CreepJS des styles calculés énumère les noms de CSSStyleDeclaration et lit séparément les couleurs et polices système ; il n'emprunte pas ce canal de préférences.
Les règles encodent également un couple. (monochrome) définit --monochrome: monochrome et (monochrome: 0) définit --monochrome: non-monochrome. La valeur enregistrée est donc une étiquette choisie entre deux branches, pas un booléen.
Le canal matchMedia
Le second canal pose les mêmes questions au moteur via Window.matchMedia. Chaque clé suit une chaîne ternaire, de la réponse la plus spécifique à la moins spécifique :
pointer: win.matchMedia('(pointer: fine)').matches
? 'fine'
: win.matchMedia('(pointer: coarse)').matches
? 'coarse'
: win.matchMedia('(pointer: none)').matches
? 'none'
: undefined;
pointer et any-pointer essaient chacun trois branches ; color-gamut essaie rec2020, puis p3, puis srgb ; display-mode essaie fullscreen, standalone, minimal-ui, puis browser. Si aucune branche ne correspond, la clé vaut undefined, le même marqueur que dans le premier canal. Les deux objets peuvent ainsi être comparés directement, clé par clé.
Quelle fenêtre chaque canal interroge
Quatorze clés ne signifient pas quatorze portées identiques. La plupart des requêtes passent par win.matchMedia, où win est la référence à la fenêtre du collecteur. Deux font exception : prefers-color-scheme et orientation appellent l’identifiant global matchMedia. Un commentaire dans la source précise que la fenêtre principale est préférée.
Ce choix est délibéré et observable. Un cadre peut se trouver dans un document dont son propre contexte rapporte un schéma de couleurs ou une orientation différente. Pour obtenir la réponse de la fenêtre conteneur, le collecteur doit interroger cette fenêtre plutôt que le cadre. Lorsqu’on compare deux exécutions, il faut noter la portée utilisée par chaque clé : une différence de prefers-color-scheme peut provenir de la portée autant que de la configuration.
La même asymétrie existe dans le canal @media pour une raison structurelle. Les règles de cascade s’appliquent au document qui possède l’élément style ; un cadre qui lit son propre corps ne voit pas une règle écrite dans le corps du document parent. Les deux canaux interrogent donc des portées différentes pour la même clé, et un désaccord ne prouve pas automatiquement une incohérence.
| Clé | Source de la valeur @media | Source de la valeur matchMedia |
|---|---|---|
prefers-reduced-motion | no-preference ou reduce | no-preference ou reduce |
prefers-color-scheme | light ou dark | light ou dark, interrogé dans la fenêtre principale |
monochrome | monochrome ou non-monochrome | les deux mêmes étiquettes |
inverted-colors | inverted ou none | les deux mêmes étiquettes |
forced-colors | none ou active | les deux mêmes étiquettes |
any-hover | hover ou none | les deux mêmes étiquettes |
hover | hover ou none | les deux mêmes étiquettes |
any-pointer | fine, coarse ou none | les trois mêmes étiquettes |
pointer | fine, coarse ou none | les trois mêmes étiquettes |
device-aspect-ratio | la chaîne de rapport réduite | la même chaîne de rapport |
device-screen | width x height | la même paire |
display-mode | fullscreen, standalone, minimal-ui ou browser | les quatre mêmes étiquettes |
color-gamut | srgb, p3 ou rec2020 | les trois mêmes étiquettes |
orientation | landscape ou portrait | les deux mêmes, interrogés dans la fenêtre principale |
Ce tableau est la partie du rapport qu’on peut vérifier sans rien exécuter : si une clé d’un objet enregistré possède une valeur absente de sa colonne, cette valeur ne vient pas de ce canal.
Ce qu’une réponse enregistrée établit ou non
Un objet contenant quatorze clés ressemble à une mesure. C’est plutôt un ensemble d’étiquettes ; les questions utiles portent sur les étiquettes absentes et sur le sens de cette absence.
undefined est le résultat d’une branche
Chaque clé finit par valoir undefined lorsqu’aucune branche ne correspond, et le collecteur conserve cette valeur telle quelle. Un rapport distingue ainsi trois états pour une clé : une étiquette correspondante, une clé absente parce qu’aucune branche ne correspond, et un objet entier jamais affiché parce que toutes les clés ont échoué. Seul le troisième état constitue une condition de blocage.
Lorsqu’on consigne une exécution, il faut conserver les deux objets séparément au lieu de les fusionner. Un objet fusionné ne permet pas de savoir si pointer: fine vient de la cascade ou de matchMedia ; cette provenance est précisément l’information ajoutée par le second canal.
La branche de blocage
La fonction d’affichage vérifie les deux objets avant de dessiner quoi que ce soit. Pour chaque canal, elle compte les clés dont la valeur est vraie et, si le compte vaut zéro, imprime une note de blocage plutôt qu’une liste vide :
!Object.keys(mediaCSS).filter((key) => !!mediaCSS[key]).length ? HTMLNote.BLOCKED : modal(...)
La distinction compte parce que les deux lectures n’ont pas les mêmes causes. Un objet @media vide signifie que la cascade n’a appliqué aucune des trente règles ; le problème peut venir de l’élément style, du document ou d’une restriction des feuilles de style. Un objet matchMedia vide signifie qu’aucune branche n’a correspondu aux quatorze requêtes. C’est très inhabituel dans une session de bureau normale et cela décrit donc un contexte très restreint, pas une préférence manquante.
Si le rapport affiche un blocage pour un canal et des étiquettes pour l’autre, il décrit un problème de portée ou de feuille de style, pas un réglage de confidentialité. Il faut d’abord le traiter comme un constat de configuration.
Ce que signifie un désaccord
La correspondance des clés permet de comparer chaque valeur sans instrumentation supplémentaire. Trois résultats sont à distinguer :
- Les deux canaux renvoient la même étiquette. Le moteur est cohérent avec lui-même à travers deux chemins de code ; cela constitue une preuve de cohérence pour cette clé, et rien de plus.
- Un canal renvoie une étiquette et l’autre
undefined. Il faut vérifier la portée d’abord :prefers-color-schemeetorientationsont interrogés dans la fenêtre principale, si bien qu’un cadre dans son propre contexte peut produire exactement ce résultat. - Les deux canaux renvoient des étiquettes contradictoires. C’est le cas le plus intéressant, mais il décrit la feuille de style et l’état média du document, pas un appareil physique.
Aucun de ces résultats ne nomme une personne, un modèle ou un verdict. Les groupes de signaux utilisés par un détecteur classent des résultats de ce type en familles. La différence est qu’une étiquette de famille est attribuée après une décision, tandis qu’une comparaison de canaux est disponible avant celle-ci.
La ligne sur l’appareil tactile
Le panneau rendu ajoute une ligne dérivée, touch device, qui lit une seule valeur enregistrée : lorsque any-pointer vaut coarse, il affiche true, sinon il montre une note indiquant une valeur inconnue plutôt que false. Un pointeur principal grossier est courant sur les téléphones et les tablettes et se trouve aussi sur certains écrans tactiles de bureau. Cette ligne suggère donc une capacité du pointeur ; elle n’affirme pas une classe de matériel.
Le balayage de la plage d’écran
La troisième partie du collecteur ne se contente pas de lire screen.width et screen.height. Elle vérifie ces valeurs au moyen de requêtes média.
La sonde à candidat unique
La première tentative formule deux requêtes correspondant exactement aux valeurs rapportées par l’objet d’écran : (device-width: W px) et (device-height: H px). Si les deux répondent, la paire est renvoyée sans changement et le balayage s’arrête.
Le balayage ne commence que si l’une des deux requêtes échoue. Cet ordre rend le cas courant peu coûteux et réserve le chemin plus lourd au diagnostic, plutôt que d’en faire le comportement par défaut.
Le parcours de dix plages
La solution de repli construit mille règles par étape et parcourt dix étapes, de zéro à dix mille pixels, afin de compléter la dimension qui n’a pas encore répondu. Les deux dimensions sont recherchées indépendamment pendant le même parcours : si la largeur répond dans la première plage, la recherche de hauteur continue. Cette méthode ne se limite pas aux requêtes média : le collecteur vérifie des valeurs au lieu de leur faire confiance, et la surface d’empreinte CSS énumère les requêtes qui rendent cette vérification possible.
Le résultat est une paire de nombres récupérés à partir des requêtes de périphérique propres au moteur, et non de l’objet screen. C’est ce qui rend cette lecture utile : une configuration qui rapporte une résolution par screen et une autre par device-width produit deux nombres différents dans le même rapport, où le désaccord est directement visible.
Le rapport d’aspect et le test sans barre des tâches
Deux valeurs dérivées proviennent du même bloc. Le rapport est calculé en réduisant la largeur et la hauteur au moyen d’une fonction de plus grand commun diviseur, puis en formatant le résultat sous la forme W/H. Cette valeur est ensuite interrogée avec matchMedia et écrite dans une règle @media.
La seconde valeur est une heuristique sur la surface disponible. Le collecteur calcule si les largeur et hauteur rapportées sont égales aux largeur et hauteur disponibles. Il traite une grande surface d’affichage sans différence entre les deux comme une condition de plus faible entropie :
const noTaskbar = !(width - availWidth || height - availHeight);
if (screen.width !== width || (width > 800 && noTaskbar)) {
LowerEntropy.IFRAME_SCREEN = true;
}
Ce marqueur est intéressant pour deux raisons. Une session de bureau réserve normalement une partie de la surface à une barre des tâches ou à un dock ; une égalité exacte sur un grand écran a donc la forme d’une surface synthétique. De plus, le marqueur est écrit dans un enregistrement partagé qui ne déclare que six clés, dont aucune n’est celle-ci. Il s’agit donc d’une annotation ad hoc, pas d’un champ documenté. Si d’autres scripts lisent cet enregistrement, la présence d’une clé ne prouve pas qu’elle était prévue.
| Champ | Origine | Ce qu’il établit |
|---|---|---|
screenQuery.width | premier candidat, puis dix plages de 1000 px | la réponse du moteur à device-width |
screenQuery.height | le même parcours, indépendamment | la réponse du moteur à device-height |
device-aspect-ratio | largeur et hauteur réduites par leur plus grand commun diviseur | une chaîne de rapport, pas un type d’écran |
device-screen | W x H interrogé avec matchMedia | si la paire correspond comme une seule condition |
LowerEntropy.IFRAME_SCREEN | écran rapporté comparé à la surface disponible | un indice d’affichage à faible entropie, pas un verdict |
La ligne tactile et le parcours de plages décrivent l’affichage. La géométrie de l’écran et de la fenêtre forme une autre surface, avec ses différences entre résolution rapportée, surface disponible et fenêtre d’affichage de mise en page. L’empreinte de l’écran et de la fenêtre couvre ces valeurs ; ce collecteur n’en vérifie que deux au moyen de requêtes média.
Un relevé jumelé pour une exécution
Les deux objets du collecteur permettent de créer facilement un relevé, à condition de conserver les deux.
Champs à conserver
Enregistre les deux objets sous des clés distinctes, la paire d’écran récupérée par le balayage, l’identifiant de compilation du navigateur et l’URL du document. Ajoute un champ que le collecteur ne produit pas : la portée dans laquelle chaque clé sensible a été interrogée. Ce champ transforme un désaccord en explication plutôt qu’en alarme.
Conserve les étiquettes brutes au lieu d’un hash. Le collecteur affiche un hash court à côté de chaque panneau modal ; c’est pratique pour comparer rapidement deux rapports, mais inutile pour en diagnostiquer un seul, car un hash de quatorze étiquettes ne permet pas de voir laquelle a changé.
Répéter et comparer
Deux exécutions avec la même configuration devraient produire les mêmes étiquettes pour chaque clé. Une clé qui change entre les exécutions révèle une différence de configuration ou de synchronisation : la feuille de style peut ne pas s’être appliquée pendant l’une des exécutions, le cadre peut avoir été dans une autre portée, ou le document peut avoir été chargé dans un autre environnement.
Compare clé par clé plutôt qu’objet par objet, et consigne le sens du changement. Un pointer qui passe de fine à undefined révèle autre chose qu’un passage de fine à coarse ; seul le second décrit une capacité du pointeur qui a changé.
Ce que le relevé ne peut pas trancher
Le relevé décrit ce que le moteur du document a répondu. Il ne décrit ni le matériel d’affichage, ni le système d’exploitation, ni le modèle qu’un site construit à partir de ces étiquettes. Une comparaison propre entre les deux canaux indique seulement que les deux chemins de code ont concordé pour cette compilation et ce document : c’est une affirmation de cohérence, pas d’identité.
La référence des signaux CSS documente les paramètres disponibles. Cela ne change pas la conclusion d’un collecteur tiers face à un désaccord et ne peut pas faire se comporter un écran à faible entropie comme un panneau physique particulier.
Questions fréquentes
Les deux canaux concordent-ils toujours ?
Pas nécessairement ; les raisons sont structurelles, pas aléatoires. Le canal de cascade ne s’applique qu’au document propriétaire de l’élément style, alors que deux clés matchMedia sont interrogées dans la fenêtre principale. Un cadre situé dans un autre contexte peut légitimement produire une réponse différente pour la même clé.
Que signifie une note de blocage ?
Elle signifie que toutes les clés de ce canal ont renvoyé une valeur fausse. Pour le canal de cascade, cela indique que la feuille de style ne s’est pas appliquée ; pour le canal matchMedia, cela indique un contexte restreint, car une session de bureau normale correspond à au moins une branche pour la plupart des quatorze clés.
Pourquoi interroger les requêtes média plutôt que lire les valeurs d’écran ?
Parce que la sonde fournit une réponse indépendante à la même question. Une configuration peut rapporter une résolution par screen et une autre par device-width ; le balayage rend cette différence visible dans le rapport au lieu de la laisser à un test séparé.
Conclusion pratique
Commence par les deux objets plutôt que par le panneau rendu. Un canal vient de la cascade, l’autre d’une fonction de correspondance ; ils répondent aux mêmes quatorze questions, et chaque clé peut être absente, étiquetée ou appartenir à un canal qui n’a jamais été rendu. Lis ensuite les valeurs d’écran comme une mesure : le balayage vérifie la paire rapportée par rapport à device-width et device-height, tandis que le test sans barre des tâches indique la surface disponible, pas un verdict.
L’étape suivante consiste à créer un relevé jumelé pour un document que tu contrôles. Conserve les deux objets, la paire d’écran récupérée et la portée utilisée par chaque clé sensible, puis répète l’exécution et compare clé par clé. Si une clé change, examine la feuille de style ou la portée ; si toutes restent stables, les deux canaux ont concordé sur cette compilation.
Sources
Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?
Articles Connexes
Fingerprinting CSS : comment les styles vous suivent
PlateformePréférences des media queries : respecter les choix des utilisateurs
EmpreinteCollecteur CSS CreepJS : styles calculés
EmpreinteEmpreinte d'écran et de fenêtre : suivi de l'affichage
EmpreinteLes groupes de signaux de CreepJS et leur combinaison
EmpreinteSignaux Math de CreepJS : lire les champs
Exécutez la même vérification dans BotBrowser
Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.