Browser Fingerprint Viewer — Ruby source
See exactly what websites can learn about your browser without cookies — screen, GPU, fonts, timezone, language, and more. Educational, not tracking.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# Browser Fingerprint — pure logic (Ruby port): the 18-signal privacy-risk
# registry with canonical grouping, risk counts, and a stable SHA-256
# fingerprint hash.
#
# Language: Ruby 3.2 — standard library only (digest/sha2 for SHA-256).
# Source: CosmoDev polyglot showcase port; canonical = src/lib/browser-fingerprint.ts
# + this tool's python.py / rust.rs.
# License: display source — part of CosmoDev's polyglot tool pages.
#
# Ported from the TypeScript reference. Covers the portable core: the
# 18-signal registry with privacy-risk classifications, canonical category
# grouping, risk counts, and the stable SHA-256 fingerprint hash. The
# browser-API collection layer (canvas / WebGL / font probes) is DOM-bound
# by design and does not port.
require 'digest'
module BrowserFingerprint
# Canonical category order (card grid, top to bottom).
FINGERPRINT_CATEGORIES = %i[hardware graphics network browser input].freeze
# Category display labels, keyed by the canonical category symbols.
CATEGORY_LABELS = {
hardware: 'Hardware',
graphics: 'Graphics',
network: 'Network',
browser: 'Browser',
input: 'Input'
}.freeze
# One collected, classified signal.
Signal = Struct.new(:id, :label, :value, :risk, :category, keyword_init: true)
# Classifies one signal id.
SignalDef = Struct.new(:label, :risk, :category, keyword_init: true)
# One category and its signals.
CategoryGroup = Struct.new(:category, :signals, keyword_init: true)
# Signals per risk level (drives the summary line).
RiskCounts = Struct.new(:low, :medium, :high, keyword_init: true)
# Static registry of every signal the tool collects, with its privacy-risk
# classification. Single source of truth: build_signal derives from it, so
# a signal can never be collected without being classified.
SIGNAL_DEFINITIONS = {
# High risk
'canvas' => SignalDef.new(label: 'Canvas fingerprint', risk: :high, category: :graphics),
'webgl-renderer' => SignalDef.new(label: 'WebGL renderer', risk: :high, category: :graphics),
'webgl-vendor' => SignalDef.new(label: 'WebGL vendor', risk: :high, category: :graphics),
'fonts' => SignalDef.new(label: 'Installed fonts', risk: :high, category: :browser),
'timezone' => SignalDef.new(label: 'Timezone', risk: :high, category: :browser),
# Medium risk
'screen' => SignalDef.new(label: 'Screen & color depth', risk: :medium, category: :hardware),
'device-pixel-ratio' => SignalDef.new(label: 'Device pixel ratio', risk: :medium, category: :hardware),
'hardware-concurrency' => SignalDef.new(label: 'CPU cores', risk: :medium, category: :hardware),
'device-memory' => SignalDef.new(label: 'Device Memory', risk: :medium, category: :hardware),
'platform' => SignalDef.new(label: 'Platform', risk: :medium, category: :browser),
'languages' => SignalDef.new(label: 'Languages', risk: :medium, category: :browser),
'touch' => SignalDef.new(label: 'Touch support', risk: :medium, category: :input),
'connection' => SignalDef.new(label: 'Connection type', risk: :medium, category: :network),
# Low risk
'user-agent' => SignalDef.new(label: 'User agent', risk: :low, category: :browser),
'do-not-track' => SignalDef.new(label: 'Do Not Track', risk: :low, category: :browser),
'cookies-enabled' => SignalDef.new(label: 'Cookies enabled', risk: :low, category: :browser),
'online' => SignalDef.new(label: 'Online status', risk: :low, category: :network),
'pdf-viewer' => SignalDef.new(label: 'PDF viewer', risk: :low, category: :browser)
}.freeze
module_function
# Risk level for a known signal id; nil for unknown ids.
def classify_signal_risk(signal_id)
SIGNAL_DEFINITIONS[signal_id]&.risk
end
# Build a signal from its id and collected value. Raises ArgumentError on
# unknown ids so a typo'd id fails loudly instead of silently rendering an
# unclassified row.
def build_signal(signal_id, value)
definition = SIGNAL_DEFINITIONS.fetch(signal_id) do
raise ArgumentError, "Unknown fingerprint signal id: #{signal_id}"
end
Signal.new(
id: signal_id,
label: definition.label,
value: value,
risk: definition.risk,
category: definition.category
)
end
# Group signals by category in canonical order, omitting empty categories.
def group_by_category(signals)
by_category = signals.group_by(&:category)
FINGERPRINT_CATEGORIES.filter_map do |category|
members = by_category[category]
CategoryGroup.new(category: category, signals: members) if members
end
end
# Count signals per risk level.
def count_by_risk(signals)
counts = Hash.new(0)
signals.each { |signal| counts[signal.risk] += 1 }
RiskCounts.new(low: counts[:low], medium: counts[:medium], high: counts[:high])
end
# The exact byte string the fingerprint hash is computed over: every value
# joined with '|' in signal order.
def concat_signal_values(signals)
signals.map(&:value).join('|')
end
# SHA-256 hex digest of the UTF-8 encoding of text.
def sha256_hex(text)
Digest::SHA256.hexdigest(text)
end
# Stable fingerprint ID: SHA-256 over every signal value, joined in signal
# order. Same browser state -> same hash; any single changed value -> new hash.
def hash_fingerprint(signals)
sha256_hex(concat_signal_values(signals))
end
end
if $PROGRAM_NAME == __FILE__
signals = [
BrowserFingerprint.build_signal('timezone', 'Europe/Paris'),
BrowserFingerprint.build_signal('screen', '2560×1440 @ 24-bit'),
BrowserFingerprint.build_signal('user-agent', 'Mozilla/5.0 (Macintosh)')
]
BrowserFingerprint.group_by_category(signals).each do |group|
puts "[#{BrowserFingerprint::CATEGORY_LABELS[group.category]}]"
group.signals.each do |signal|
puts " #{signal.label}: #{signal.value} (#{signal.risk} risk)"
end
end
counts = BrowserFingerprint.count_by_risk(signals)
puts "risk mix: #{counts.high} high / #{counts.medium} medium / #{counts.low} low"
puts "fingerprint: #{BrowserFingerprint.hash_fingerprint(signals)}"
end
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →