# browser_class

> WhiskerJS's own guess at the browser family and its privacy mode, one of ten classes. It picks the exclusion row and joins the id.

Source: https://whiskerjs.com/signals/browser-class/ (last updated 2026-10-26)

**browser_class at a glance (from the library code, signalsVersion 9)**

| Field | Value |
| --- | --- |
| Component name | `browser_class` |
| In the visitorId | yes |
| structuredId group | [browser (engine and build)](https://whiskerjs.com/docs/identifiers/#structuredid) |
| 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](https://whiskerjs.com/whats-new/) |

> **In plain words:**
>
> Before measuring anything, WhiskerJS makes a quick guess at which kind of browser it is in, and whether a privacy mode is on: Brave, Firefox (stock, with Fingerprinting Protection, or with its strict anti-fingerprinting mode), Tor Browser or Mullvad Browser, Safari (stock or with Advanced Fingerprinting Protection), a Chromium-based browser, an Android in-app browser (WebView), 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 and from three tiny test reads, not proof.

## What it reads

- `navigator.brave` and whether `navigator.brave.isBrave` is a function;
- the user agent string (`navigator.userAgent`);
- whether `mozInnerScreenX` exists on the window (a Firefox-only property);
- `navigator.vendor`;
- whether `window.chrome` exists;
- the brands in `navigator.userAgentData` (for Android WebView);
- for Firefox only, the [resistFingerprinting](https://whiskerjs.com/glossary/#resistfingerprinting) traits: the Intl time zone, `screen` and `avail*` sizes, `innerWidth` and `innerHeight`, `devicePixelRatio`, `screenX`, `screenY`, `navigator.webdriver` and, for Tor and Mullvad, `navigator.languages`;
- the results of the canvas and WebGL [known-answer probes](https://whiskerjs.com/signals/noise-profile/), which run once per collection before the collectors (they are skipped after an abort).

## How the technique works

The checks run in this order and the first match wins. There are ten classes, exported as `BROWSER_CLASSES`:

1. **brave**: `navigator.brave.isBrave` is a function.
2. **Firefox**: the user agent contains `Firefox/` followed by a number, or the window has `mozInnerScreenX`. Then:

   - **firefox_rfp** when every one of these holds at once: the time zone is `UTC`, `Etc/UTC` or `Atlantic/Reykjavik`; `navigator.webdriver` is not true; the screen is exactly the size of the content area (`innerWidth`, `innerHeight`); the available screen equals the screen; `devicePixelRatio` is 1; and `screenX` and `screenY` are 0;
   - **rfp_hardened** (Tor Browser, Mullvad Browser) when, on top of those traits, the content area sits on the letterboxing steps (width a multiple of 200, height a multiple of 100 CSS pixels) and `navigator.languages` is `en-US` or `en-US,en`;
   - **firefox_fpp** (Fingerprinting Protection) when it is not resistFingerprinting but the canvas or WebGL known-answer probe came back altered (`seeded` or `per_read`);
   - otherwise **firefox**.
3. **Chromium**: the user agent contains `Chrome/` or `Chromium/` followed by a number at a word boundary, or names only `HeadlessChrome/` (since signalsVersion 9), or `window.chrome` exists and the vendor is `Google Inc.`. Then **chromium_webview** when the user agent’s platform part ends in `; wv)` or a `userAgentData` brand is `Android WebView`; otherwise **chromium**.
4. **Safari**: the vendor is `Apple Computer, Inc.`, or the user agent has both `Safari/` and `Version/`. Then **safari_afp** (Advanced Fingerprinting Protection) when the canvas or WebGL known-answer probe came back altered; otherwise **safari**.
5. **other**: none of the above, or something threw.

The class picks a row of the [exclusion table](https://whiskerjs.com/docs/how-it-works/#the-exclusion-table): `brave` removes twelve components, `firefox_rfp` and `rfp_hardened` nine each, `safari_afp` five, `firefox_fpp` three, and `chromium_webview`, `firefox`, `safari`, `chromium` and `other` none.

## Why it helps

The class separates engine families and privacy modes 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 Fingerprinting Protection and resistFingerprinting, Safari Advanced Fingerprinting Protection) out of the id instead of letting them change it on every visit. `chromium_webview` removes nothing; the class itself tells your server that the page runs in an Android in-app browser.

## Stability

It changes when the browser changes family, when a user switches a protection mode on or off (for example Firefox turning Fingerprinting Protection on in normal windows: the class becomes `firefox_fpp` and that row’s components are excluded), when a private window turns on a protection, or when a user agent switcher changes what the browser claims.

The resistFingerprinting 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. A stock resistFingerprinting Firefox in English whose window happens to sit on the letterboxing steps is classed `rfp_hardened`; the two rows exclude the same components, so only the label differs. Stock Firefox and stock Safari read the known answers back exactly, also under automation, and keep their plain class. See [What’s new](https://whiskerjs.com/whats-new/).

## Browser support and what blocks it

| Browser | Class |
| --- | --- |
| [Chrome desktop](https://whiskerjs.com/browsers/chrome-desktop/), [Chrome Android](https://whiskerjs.com/browsers/chrome-android/), [Edge](https://whiskerjs.com/browsers/edge/), [Samsung Internet](https://whiskerjs.com/browsers/samsung-internet/) | `chromium` (their user agents carry `Chrome/`) |
| Android WebView (in-app browsers) | `chromium_webview` |
| Headless Chromium with a `HeadlessChrome` user agent | `chromium` (it was `other` until signalsVersion 8) |
| [Brave](https://whiskerjs.com/browsers/brave/) | `brave` |
| [Firefox](https://whiskerjs.com/browsers/firefox/) | `firefox`; `firefox_fpp` when Fingerprinting Protection (or a canvas-noise extension) alters canvas or WebGL readback |
| [Firefox with resistFingerprinting](https://whiskerjs.com/browsers/firefox-rfp-and-tor/) | `firefox_rfp` when every trait holds, else `firefox` |
| [Tor Browser, Mullvad Browser](https://whiskerjs.com/browsers/firefox-rfp-and-tor/) | `rfp_hardened` |
| [Safari on macOS](https://whiskerjs.com/browsers/safari-macos/) | `safari`; `safari_afp` when Advanced Fingerprinting Protection alters canvas or WebGL readback |
| [Every iOS browser](https://whiskerjs.com/browsers/safari-ios/) (Safari, Chrome `CriOS`, Firefox `FxiOS`, Edge `EdgiOS`) | `safari` or `safari_afp`: they all report the Apple vendor and carry no `Chrome/` or `Firefox/` token |

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. The privacy classes (`firefox_fpp`, `firefox_rfp`, `rfp_hardened`, `safari_afp`) say that the user has turned on, or uses a browser with, a fingerprinting protection.

## How WhiskerJS handles noise and failures

| Status | ruleId | When |
| --- | --- | --- |
| `ok` |   | always when it runs: one of the ten 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`](https://whiskerjs.com/docs/integrity/#engine_mismatch) compares the engine behind the class (V8 for `brave`, `chromium` and `chromium_webview`; Gecko for the Firefox classes; WebKit for the Safari classes) 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`.

For the [previous ids](https://whiskerjs.com/docs/identifiers/#previous-ids-linking-across-a-version-bump), headless Chromium without `window.chrome` records `other`, the value signalsVersion 8 reported.

## Example value

A normal desktop Chrome, and headless Chromium since signalsVersion 9, report:

```json
"browser_class": { "status": "ok", "value": "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.

### What class does headless Chromium get?

chromium, since signalsVersion 9. Its user agent names only HeadlessChrome/ and the old headless mode has no window.chrome object, so until signalsVersion 8 it was classed other. The headless integrity flags report the rest.

### How does WhiskerJS tell Firefox or Safari privacy protection from the stock browser?

With the canvas and WebGL known-answer probes. Stock Firefox and stock Safari read the known answers back exactly and keep the classes firefox and safari. When a probe comes back altered, Firefox is classed firefox_fpp (Fingerprinting Protection) and Safari safari_afp (Advanced Fingerprinting Protection).

### 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.

## Related pages

- [The exclusion table](https://whiskerjs.com/docs/how-it-works/#the-exclusion-table): What each class removes, and why.
- [Firefox resistFingerprinting, Tor and Mullvad](https://whiskerjs.com/browsers/firefox-rfp-and-tor/): The strict test for the firefox_rfp and rfp_hardened classes.
- [noise_profile](https://whiskerjs.com/signals/noise-profile/): The known-answer probes that pick firefox_fpp and safari_afp.
- [Brave](https://whiskerjs.com/browsers/brave/): Recognised by navigator.brave.isBrave.
- [engine_mismatch flag](https://whiskerjs.com/docs/integrity/#engine_mismatch): Fires when the code behaves like a different engine than the class says.
- [vendor](https://whiskerjs.com/signals/vendor/): One of the inputs to the class.
