TL;DR: v3 fingerprints are different from v2 fingerprints for the same browser. The change is intentional (two underlying bugs were fixed) and the new outputs are stable. If you only use fingerprints for the current session, no action is needed. If you store fingerprints and match them later, read the Stored fingerprints section.
Two bugs in the v2 audio pipeline made every output unintentionally different from what the implementation claimed to produce. v3 fixes both — which means a v3 fingerprint for a given browser will not match the v2 fingerprint for the same browser.
1. Audio output was prefixed with the literal string "null" (#23)
The audio reducer initialized its accumulator to null and used +=. JavaScript coerced the first concatenation to "null" + 0.0001…, so every audio fingerprint started with the literal four characters "null" before the actual sum.
// v2 audio output
'null0.05000000000000004';
// v3 audio output (the bug fix)
'0.05000000000000004';That changes the input to the cyrb53 hash, which changes the final fingerprint.
2. Audio module had a concurrency bug (#22)
The audio module used module-scoped mutables for state — two overlapping calls would race on the same buffer. v3 scopes all state inside getAudioFingerprint(), so concurrent calls now resolve independently. This doesn't usually change the value for a single sequential call, but it removes a class of nondeterminism that was producing slightly different outputs in apps that called the function in parallel.
| Area | v2 behavior | v3 behavior |
|---|---|---|
| Default audio output | Prefixed with "null" |
Numeric string only |
| Concurrency | Shared module state | Per-call state |
package.json bin field |
Pointed at nonexistent script | Removed |
| Dead code | webRTC.js, FingerPrint.ts, murmurhash3_32_gc, javaHashCode shipped in source |
Removed (not in any export path) |
@ts-nocheck directives |
Present on hash + audio modules | Removed; modules fully typed |
Return type of internal cyrb53 |
Documented as string, returned number |
Documented and returns number |
If you persist fingerprints in a database for visitor identification, every stored v2 fingerprint will fail to match the v3 fingerprint for the same browser. Pick a strategy:
Vendor both versions for a transition window and match against either.
// install both: the v3 version (current) and a pinned v2 copy
import { getCurrentBrowserFingerPrint as v3 } from '@rajesh896/broprint.js';
import { getCurrentBrowserFingerPrint as v2 } from '@rajesh896/broprint.js@2.2.1';
const [fpV2, fpV3] = await Promise.all([v2(), v3()]);
const known = await db.findOne({ $or: [{ fingerprint: fpV2 }, { fingerprint: fpV3 }] });
if (known) {
// recognized — opportunistically upgrade the stored value to v3
await db.update(known.id, { fingerprint: fpV3, fingerprint_version: 3 });
} else {
// new visitor — store the v3 value
await db.insert({ fingerprint: fpV3, fingerprint_version: 3 });
}After a few weeks of overlap, the v2 vendoring can be removed.
If your fingerprints already age out (e.g. tied to short-lived analytics sessions), just deploy v3 and let the old values expire naturally. New sessions get v3 fingerprints; old ones drop off.
Store the fingerprint version alongside the value:
{ fingerprint: '1234567890123456', version: 3 }Match only against the same version. Older values become unmatchable but don't cause false negatives against new ones.
These are additive — existing v2 call sites keep working.
// v2 — still works in v3
const fp = await getCurrentBrowserFingerPrint();
// v3 — new
const fp = await getCurrentBrowserFingerPrint({
useAudio: true, // default true
useCanvas: true, // default true
seed: 0 // default 0 — namespace fingerprints per app
});See the README for the full options table.
The individual building blocks are now exported, so you can compose your own pipeline:
import {
getCanvasFingerprint,
getAudioFingerprint,
cyrb53,
isCanvasSupported
} from '@rajesh896/broprint.js';Combined with "sideEffects": false in package.json, ESM bundlers can tree-shake the parts you don't use.
The public API is now fully typed (the @ts-nocheck directives in v2 hid type errors from consumers). Type information ships in lib/index.d.ts. The BroprintOptions interface is exported.
- Read the Stored fingerprints section and pick a strategy if you persist fingerprints.
- Update your dependency:
npm install @rajesh896/broprint.js@^3. - If you import from a non-default path (rare), check that you're using the new exports.
- If you have TypeScript and were silently relying on the v2
as unknown as stringlie, the return type is nowPromise<string>(correct) — usage sites typed againststringwill keep working. - Consider switching to the named exports + tree-shaking if you only need one signal.