The visitorId is one code made from all the stable signals: if two visits give the same code, the browser setup is the same. The structuredId is the same idea split into seven short codes, one per group of signals. When only one of the seven changes (say, graphics after a driver update), you can still see that it is very likely the same device, and you can see what changed.
Every result carries two identifiers derived from the same 37 stable components:
- visitorId: one 128-bit hash. Two results either match or they do not.
- structuredId: seven short hashes, one per signal group. Two results can match in some groups and differ in others, which tells you what changed.
Neither is stored by the library, and neither is a person: they describe a browser setup. Two identical devices with identical settings can share an id, and one person with two browsers has two.
visitorId
Plainly: put every stable component on its own line, in alphabetical order, and hash the lot. Same environment, same id.
Exactly (idVersion 2):
whiskerjs/id/2
apple_pay=v:none
audio=h:0a4d8e691d7e59afff0d9a1a9d405e3b
browser_class=v:chromium
canvas=!
...
webgpu=h:862da19ca084b185c1d8c76af676a08b
- the first line is
whiskerjs/id/<idVersion>; - then one line per stable component in alphabetical order:
name=v:valuefor a plain value,name=h:digestfor a hashed one, andname=!for every status other thanok; - lines are joined with
\n, UTF-8 encoded, hashed with SHA-256, and the first 128 bits are returned as 32 lowercase hex characters.
Because every non-ok status gives the same !, a component that is
blocked in one run and timed_out in the next does not change the id. A
component that is ok in one run and not in the next does.
It is null when crypto.subtle is missing or fails (for example on a page
that is not a secure context).
structuredId
Format:
2_2ac06a6b0b67_63695d5858ec_9c9b9c3f5a2e_5e78835d0b60_77ab287db153_8ad7c1a2e06b_4880096d1ea0
| gpu audio fonts device browser locale prefs
idVersion
<idVersion>_<gpu>_<audio>_<fonts>_<device>_<browser>_<locale>_<prefs>.
Each part is the first 48 bits (12 hex characters) of SHA-256 over the line
whiskerjs/id/2/<group> followed by the group’s components in alphabetical
order, one line each in the visitorId encoding above.
| Group | Components |
|---|---|
gpu |
canvas, webgl_image, webgl_params, webgl_renderer, webgl_vendor, webgpu |
audio |
audio |
fonts |
font_preferences, fonts, fonts_canvas |
device |
color_depth, cpu_architecture, device_memory, hardware_concurrency, heap_limit, screen_resolution, touch_support |
browser |
apple_pay, browser_class, math, media_codecs, pdf_viewer, platform, plugins, private_click, storage, ua_client_hints, vendor, vendor_flavors |
locale |
intl_locale, languages, time_zone |
prefs |
color_gamut, dynamic_range, forced_colors, prefers_contrast, reduced_motion |
The groups and their order are exported as ID_GROUPS and
ID_GROUP_ORDER, the part length as ID_PART_LENGTH (12). The
structuredId is null without a digest.
Comparing two results
WhiskerJS only derives the parts. Deciding what counts as “the same device” is your server’s job. A simple, explainable approach:
import { ID_GROUP_ORDER } from 'whiskerjs';
/** Which groups match between two structuredIds of the same idVersion. */
function compareStructured(a: string, b: string): Record<string, boolean> | null {
const pa = a.split('_');
const pb = b.split('_');
if (pa[0] !== pb[0]) return null; // different idVersion: not comparable
const out: Record<string, boolean> = {};
ID_GROUP_ORDER.forEach((group, i) => {
out[group] = pa[i + 1] === pb[i + 1];
});
return out;
}
// { gpu: false, audio: true, fonts: true, device: true, browser: true, locale: true, prefs: true }
// reads as: "same device and settings, the graphics changed" (for example a driver update)
Typical patterns, as hints and not rules:
| What differs | A likely cause |
|---|---|
only gpu |
GPU driver or browser graphics update, hardware acceleration switched |
only browser |
browser update (Client Hints brands, Math results, codecs) |
only locale |
the user changed language or time zone, or travelled |
only prefs |
an accessibility or display setting changed |
only device |
a different screen or a changed CPU or memory report |
fonts |
fonts installed or removed, a font setting changed |
| most groups | a different device, or a different browser on the same device |
When matching, weigh groups by how much they tell apart and how often they change on your own traffic. The components inside each group are in the result, so you can also compare them one by one.
What changes the id
The visitorId changes when any stable component’s value or status (ok or not ok) changes. Common causes:
- browser or operating system updates (
math,media_codecs,ua_client_hints, graphics); - GPU driver updates, or turning hardware acceleration on or off;
- installing or removing fonts;
- changing language, time zone or accessibility settings;
- a different screen as the primary display;
- a private window or an extension that adds noise to canvas or WebGL (the
component turns
noisy); - a component timing out on a slow or busy device in one run but not in another.
What does not change it: the pixel ratio (which follows page zoom), the
taskbar or dock (screen_frame), permission grants, the keyboard layout, ad
blockers, voices loading late, the audio output latency and exact browser
version numbers (ua_versions). Those components are outside the id.
Why versions bump
- signalsVersion goes up when a collector changes what it measures, an integrity check changes, or the exclusion table changes. Adding a stable component changes the visitorId of nearly every device; changing a check changes no id but changes which flags you see.
- idVersion goes up when the derivation itself changes: the line encoding, the prefix, the groups. The prefix contains the idVersion, so a new idVersion gives every device a new id.
Treat a visitorId as comparable only with ids of the same idVersion and
signalsVersion. Across versions, re-link devices through something you
already trust (a signed-in session, for example) and store the new id next
to the old one. What’s new lists what each version
changed.