Docs · WhiskerJS 0.1.0 · signalsVersion 5 · idVersion 2

Identifiers

visitorId for exact matches, structuredId for similarity, and what makes them change.

In plain words

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:value for a plain value, name=h:digest for a hashed one, and name=! for every status other than ok;
  • 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.