| Component name | browser_class |
|---|---|
| In the visitorId | yes |
| structuredId group | browser (engine and build) |
| Hashing | plain value; a digest only when longer than 128 characters or not printable ASCII |
| Excluded under | no browser class |
| Inner timeout | 1000 ms |
| Added in | signalsVersion 1 |
Before measuring anything, WhiskerJS makes a quick guess at which kind of browser it is in: Brave, Firefox with its anti-fingerprinting mode, Firefox, Safari, a Chromium-based browser, or something else. That guess decides which signals are skipped because the browser randomises them on purpose, and it is also reported as a signal of its own. It is a guess from what the browser says about itself, not proof.
What it reads
navigator.braveand whethernavigator.brave.isBraveis a function;- the user agent string (
navigator.userAgent); - whether
mozInnerScreenXexists on the window (a Firefox-only property); navigator.vendor;- whether
window.chromeexists; - for Firefox only, the resistFingerprinting traits: the Intl time zone,
screenandavail*sizes,innerWidthandinnerHeight,devicePixelRatio,screenX,screenYandnavigator.webdriver.
How the technique works
The checks run in this order and the first match wins:
- brave:
navigator.brave.isBraveis a function. - firefox or firefox_rfp: the user agent contains
Firefox/followed by a number, or the window hasmozInnerScreenX. It isfirefox_rfponly when every one of these holds at once: the time zone isUTC,Etc/UTCorAtlantic/Reykjavik;navigator.webdriveris not true; the screen is exactly the size of the content area (innerWidth,innerHeight); the available screen equals the screen;devicePixelRatiois 1; andscreenXandscreenYare 0. Otherwise it isfirefox. - chromium: the user agent contains
Chrome/orChromium/followed by a number at a word boundary, orwindow.chromeexists and the vendor isGoogle Inc.. - safari: the vendor is
Apple Computer, Inc., or the user agent has bothSafari/andVersion/. - other: none of the above, or something threw.
The class picks a row of the exclusion table: brave removes seven components, firefox_rfp removes nine, the others remove none.
Why it helps
The class separates engine families in one short word, and because it is in the id, a browser that changes family on the same device changes the id. Its main job is internal, though: it keeps randomised values (Brave farbling, Firefox resistFingerprinting) out of the id instead of letting them change it on every visit.
Stability
It changes only when the browser changes family, when a user switches Firefox resistFingerprinting on or off, or when a user agent switcher changes what the browser claims. The firefox_rfp test is deliberately strict: each trait alone is common in stock Firefox, and stock Firefox under automation (Playwright runs it in UTC with the screen equal to the viewport) matches several. A missed resistFingerprinting browser only leaves normalised values in; a false positive would drop nine components, seven of them from the id. That was fixed in signalsVersion 5; see What’s new.
Browser support and what blocks it
| Browser | Class |
|---|---|
| Chrome desktop, Chrome Android, Edge, Samsung Internet | chromium (their user agents carry Chrome/) |
| Brave | brave |
| Firefox | firefox |
| Firefox with resistFingerprinting, Tor Browser | firefox_rfp when every trait holds, else firefox |
| Safari on macOS | safari |
Every iOS browser (Safari, Chrome CriOS, Firefox FxiOS, Edge EdgiOS) |
safari: they all report the Apple vendor and carry no Chrome/ or Firefox/ token |
Headless Chromium with a HeadlessChrome user agent and no window.chrome |
other |
Nothing blocks it: the checks only read properties and never throw out of the collector.
Privacy
The class is a coarse category shared by millions of browsers. It reveals no version and no personal setting, except that firefox_rfp says the user has turned on (or uses a browser with) resistFingerprinting.
How WhiskerJS handles noise and failures
| Status | ruleId | When |
|---|---|---|
ok |
always when it runs: one of the six class names | |
excluded |
caller_excluded |
you excluded it; the exclusion table still uses the class internally |
timed_out |
timeout, aborted |
budget or abort, which is unlikely for a synchronous check |
If any read throws, the class falls back to other rather than failing. The value is never hashed. Related integrity checks: engine_mismatch compares the class with the printed length of eval (33 characters in V8, 37 in Gecko and WebKit) and navigator.productSub; it is not checked for class other.
Example value
From the headless test browsers in a Linux container: Firefox gave firefox, WebKit gave safari, and headless Chromium gave other (its user agent says HeadlessChrome and it has no window.chrome). A normal desktop Chrome reports chromium.
Questions and answers
Why is Chrome on my iPhone classed safari?
Every iOS browser runs on Apple's WebKit, reports the vendor Apple Computer, Inc. and has no Chrome/ token in its user agent (Chrome on iOS says CriOS). WhiskerJS follows the engine, so it is classed safari.
Why did headless Chromium come out as other?
Its user agent says HeadlessChrome, which does not match the Chrome/ token at a word boundary, and the old headless mode has no window.chrome object. Neither chromium rule matches, so the class is other.
Can I trust browser_class to tell me the real browser?
No. It is a guess from values the client controls. Use it as a feature, compare it with the User-Agent header your server sees, and look at the engine_mismatch flag.