Concept

Collection Lifecycle

What happens between a getFingerprint or getDeviceId call and the result it returns: filtering, collection, fallback for missing signals, and the structural guarantees on the output.

Reviewed

Overview

Every call to getFingerprint or getDeviceId passes through a single orchestrator. The orchestrator filters the registered collector list by tier, by the caller's exclusion list, and by an optional binding restriction (e.g. getDeviceId restricts to hardware-bound signals), runs the active set, and assembles a Record<string, SignalResult>.

The result is guaranteed to contain an entry for every collector that passed the filter — no collector name silently goes missing. Collectors that time out, throw, or are unsupported by the current browser produce a typed absent result with a stable sentinel value so the downstream fusion stage receives a complete map regardless of partial failures.

Several orchestration mechanics — collector ordering, batching strategy, the dispatch model used to overlap slow async APIs, and exact timeout constants — are deliberate parts of Benny's defensive posture and are not part of the public contract.

Filtering

The filtering pass removes any collector that does not match the active tier set, that the caller has explicitly excluded by name, that is incompatible with the caller's binding restriction, or that a stabilize option has marked as known-farbled on the detected browser.

The filtering pass fails closed: a per-collector error during filtering drops the collector from the active set rather than propagating, and a top-level failure during filtering yields an empty list so the orchestrator never runs an unfiltered collector pass.

typescript
// SignalCollector interface (public surface)
interface SignalCollector {
  name: string;
  tier: number;
  binding: SignalBinding;
  collect: () => Promise<SignalResult>;
}

// SignalResult shape (src/types.ts)
interface SignalResult {
  hash: string;
  confidence: 'high' | 'low' | 'absent';
  binding: SignalBinding;
  timeMs: number;
  sentinel?: 'unsupported' | 'threw' | 'timeout' | 'randomized';
  // value is included only when options.debug === true
  value?: unknown;
}

The public collector contract is collect(). Some collectors implement additional internal hooks the orchestrator uses to overlap async API latency; those hooks are not part of the public surface and may change between releases.

Running the active set

The orchestrator runs the filtered collector set under a global timeout. Per-collector failures are isolated — a collector that throws is replaced with a typed absent result with sentinel 'threw'; a collector that does not complete in time is replaced with sentinel 'timeout'. Both yield confidence: 'absent' so downstream stages can distinguish a real measurement from a missing one.

An optional warmup yield (options.warmup === true) inserts a single short idle gap before any collectors fire. This lets a first paint settle before timing-sensitive signals begin. Warmup is opt-in, never changes any signal hash, and is bounded so it cannot extend total collection time meaningfully.

An optional per-collector timeout (options.componentTimeout) wraps each collector individually so a single slow API cannot consume the whole global budget.

Result completeness and the absent fallback

After the collection pass completes (or the global timeout fires), the orchestrator walks the full filtered list and fills in any collector still missing a result with an absent value. Every collector in the filtered set appears in the output map, every time. This invariant lets the fusion stage construct the composite hash from a fixed schema regardless of partial failures.

Errors are reported on FingerprintResult.errors when they occurred. A clean run omits the field. Errors are diagnostic only and never feed into the fingerprint hash, which is determined entirely by the signal results and a fixed schema.

Result sentinels (what a missing or unstable result looks like)

sentinelMeaning
'unsupported'A browser API the collector depends on is absent in this environment.
'threw'The collector raised an exception during collection.
'timeout'The collector did not complete before its deadline.
'randomized'Noise-mitigation ran but detected per-call randomisation too severe for stable consensus; a value is still returned, marked unstable.

Things worth knowing

  • Total collection time is bounded by a global timeout. The bound is intentionally conservative; the typical wall-clock for a default tier-1 call is well below the limit.
  • getDeviceId restricts collection to hardware-bound signals only, producing a noticeably faster call than the full getFingerprint pass.
  • Warmup is opt-in and bounded. It never changes any signal hash and is safe to enable on all browsers.
  • Result completeness is a load-bearing invariant. Downstream comparison and fusion code can assume every filtered collector has an entry in the result map, even on partial failures.
  • Errors are surfaced on FingerprintResult.errors for diagnostics but do not influence the composite hash. The hash is byte-identical for a given environment whether errors occurred or not.
  • Several orchestration mechanics (collector ordering, dispatch overlap for slow async APIs, batching) are part of Benny's defensive posture against detection and profiling. These are not part of the public contract and may change between releases without notice.

Last reviewed 2026-06-04