A fingerprint tracker
Why it matters Clearing cookies feels like starting fresh. This lab shows, with AdCo's own log, why it often is not: a browser can be recognised from its traits alone. It also shows the defence that breaks it.
If someone cannot read your wristband, they can still describe you: tall, red coat, green backpack, walks with a limp. Each detail is common, but together they point to one person. A browser can be described the same way.
Browser fingerprint: a summary of many small browser details (screen size, time zone, fonts, how the graphics chip draws a test image) that together often identify one browser, with no cookie at all.
In this lab AdCo's script also reads Sam's browser traits (you switch that on with read_traits=True), and Sam clears his cookies and site storage every 3 pages (clear_every=3), so AdCo has to hand out a new cookie ID each time. Each record now has a traits dict.
Turning traits into one short label
To compare traits easily, you turn the whole dict into one short label, a hash. The same traits always give the same hash; change one trait and the hash changes completely.
Hash: a function that turns any text into a short code (here, a number). Same input, same code; different input, a very different code. The course provides hash_text(text).
There is one trap: a dict can list the same keys in a different order. json.dumps(traits, sort_keys=True) writes the keys in alphabetical order, so the same traits always become the same text before hashing.
import json
from watched.data import hash_text
a = {"screen": "1440x900", "timezone": "Europe/London"}
b = {"timezone": "Europe/London", "screen": "1440x900"} # same traits, other order
print(hash_text(json.dumps(a, sort_keys=True)) == hash_text(json.dumps(b, sort_keys=True))) # TrueTo count how many different values there are, put them in a set and take its length. Curly brackets with a loop inside build a set (a set comprehension):
ids = ["a", "b", "a", None]
print(len({i for i in ids if i is not None})) # 2The defence: farbling
Some browsers, including Brave and Floati, add tiny, harmless changes to a few traits, different for every site (checked October 2026). Pages still work, but the label made on ShopA no longer matches the one made on ShopB. You will measure that too with farble=True. The other main defence, making every browser look the same (uniformity), is the one you try yourself in lesson 50.
Farbling: adding small per-site noise to fingerprintable traits, so each site sees a slightly different browser.
In other languages
JavaScript, sorted keys then a SHA-256 hash with the Web Crypto API:
const text = JSON.stringify(Object.keys(traits).sort().reduce((o, k) => (o[k] = traits[k], o), {}));
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));Go: json.Marshal already writes map keys in sorted order, so the text is stable:
text, _ := json.Marshal(traits) // traits is a map[string]any
sum := sha256.Sum256(text)
label := hex.EncodeToString(sum[:8])Recap
- Many small traits together can identify a browser, with no cookie at all.
- Sorting keys before hashing makes the label stable.
- Clearing cookies did not hide Sam; farbling did.
Write fingerprint(traits) that returns hash_text(json.dumps(traits, sort_keys=True)), and join_by_fingerprint(records) that maps each record's fingerprint to the set of sites. Then, for seed=7 and Sam's first 12 pages: set cookie_ids to the number of different non-None cookie IDs and fingerprints to the number of different fingerprints, both from practice_log(world, pages, read_traits=True, clear_every=3); and set farbled to the number of fingerprints from practice_log(world, pages, read_traits=True, farble=True).
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.