Partition the engine
Why it matters In Leg 2 AdCo's tracker worked because AdCo's embedded frame had one storage drawer that it opened on every shop. Browsers stop that by splitting storage per site, which is called partitioning. If an engine splits cookies but forgets even one other storage area, the tracker simply moves its ID there. In this lab you are the engine author: you write the rule that decides where data is kept, then watch the tracker fail.
Picture a chain of gyms. A careless chain gives each member one locker key that opens the same locker in every branch, so anything left in it follows you around town. A careful chain gives you a separate locker in each branch. And inside each branch, every member has their own locker, so nobody can open someone else's.
A browser engine is the careful chain. The branch is the site in the address bar. The member is whoever is storing the data, such as the shop itself or an advert frame embedded in it.
Top-level site: the site shown in the address bar, for example shopa.example. Everything on the page, frames and scripts included, is loaded on its behalf.
Origin: who is storing the data: scheme, host and port, for example https://adco.example. An embedded frame has its own origin, different from the page around it.
Storage area: one place a browser keeps data for a site. Cookies are one area; localStorage, IndexedDB (a small database in the browser), the HTTP cache (saved copies of files) and service workers (scripts that run in the background for a site) are others.
Partition key: the label an engine puts on every drawer. Two reads or writes with the same key share a drawer; different keys never do.
The practice engine
The module watched.leg6b_engine has a tiny engine called MiniEngine with five storage areas. You give it one function, the partition key rule, and it asks that function every time anything is read or written:
from watched.leg6b_engine import MiniEngine, STORAGE_AREAS
def naive_key(area, origin, top_site):
return origin # one drawer per origin, on every site
engine = MiniEngine(naive_key)
engine.write("cache", "https://adco.example", "shopa.example", "adco_id", "id-77")
print(engine.read("cache", "https://adco.example", "shopb.example", "adco_id")) # id-77
print(STORAGE_AREAS) # ('cookies', 'local_storage', 'indexeddb', 'cache', 'service_worker')With the naive rule, AdCo writes its ID while you are on ShopA and reads the same ID back on ShopB. That one line is cross-site tracking.
A key made of two parts
The fix is to make the key include both the top-level site and the origin. Python has a neat way to glue values together: a tuple, written with round brackets.
Tuple: a fixed group of values, like ("shopa.example", "https://adco.example"). It works like a list you cannot change, and unlike a list it can be used as a dictionary key.
key_a = ("shopa.example", "https://adco.example")
key_b = ("shopb.example", "https://adco.example")
print(key_a == key_b) # False: different site, so a different drawerEngines call this double keying: every piece of stored data is filed under (top-level site, origin). Real engines add more to some keys, but these two parts are the heart of it.
Which areas does a naive engine forget?
Engines did not partition everything at once. Cookies got most of the attention, while caches and newer stores came later. The HTTP cache is now partitioned in all three major engines: Safari since 2013, Chrome since version 86 and Firefox since version 85 (checked October 2026). Any area left out is a hiding place, which is why your key rule must cover every area, not only cookies.
Measuring whether tracking still works
run_tracker(engine) visits the three shops. On each one the shop saves a cart, then AdCo's frame looks for its ID in every area and writes a new one if the drawer is empty. It returns a dict:
"sightings": a list of(top_site, area, id_seen)tuples, one per shop and area"snooped": areas where AdCo's frame could read the shop's own cart (should be empty)"first_party_ok": True when every shop could read its own cart back
Tracking works in an area when the same ID shows up on two or more different sites. To count that you will use a set: a collection with no repeats, written set() when empty. s.add(x) puts something in and len(s) says how many different things it holds.
seen = {} # (area, id) -> set of sites
for site, area, ident in [("shopa.example", "cache", "id-1"), ("shopb.example", "cache", "id-1")]:
seen.setdefault((area, ident), set()).add(site)
print(seen) # {('cache', 'id-1'): {'shopa.example', 'shopb.example'}}d.setdefault(key, set()) means: give me the value under this key, and if there is none yet, store an empty set there first.
A common mistake
It is tempting to key by the top-level site only: return top_site. That does stop AdCo linking ShopA to ShopB. But now AdCo's frame and the shop share one drawer on each site, so the frame can read the shop's cart. The engine report calls that snooped. A good key separates sites from each other and members from each other.
Try it The practice Browser already has this rule built in for cookies and localStorage. After this lab, rerun your Leg 2 cookie tracker with Browser(world, partitioned=True) and print the profile it builds: you should see one short profile per shop instead of one long one. In a real browser, open DevTools (F12), go to Application (Storage in Firefox) and open Cookies. In Chrome's DevTools, cookies stored per top-level site show a value in the Partition Key column.
In other languages
The same double key in other languages:
// JavaScript: a Map keyed by a string that joins both parts
const drawers = new Map();
function partitionKey(area, origin, topSite) {
return `${area} ${topSite} ${origin}`;
}
drawers.set(partitionKey("cache", "https://adco.example", "shopa.example"), { adco_id: "id-1" });
// Rust: a tuple of Strings is a valid HashMap key
use std::collections::HashMap;
let mut drawers: HashMap<(String, String), HashMap<String, String>> = HashMap::new();
drawers.entry(("shopa.example".into(), "https://adco.example".into()))
.or_default().insert("adco_id".into(), "id-1".into());
// Go: a struct with comparable fields can key a map
type PartitionKey struct{ TopSite, Origin string }
drawers := map[PartitionKey]map[string]string{}
drawers[PartitionKey{"shopa.example", "https://adco.example"}] = map[string]string{"adco_id": "id-1"}Recap
- Partitioning files every piece of stored data under a key; same key, same drawer.
- A good key has two parts, the top-level site and the origin, so sites cannot see each other and a frame cannot see the page around it.
- Every storage area needs the key, cookies, localStorage, IndexedDB, cache and service workers alike. One forgotten area is enough for a tracker.
- Tracking still works wherever the same ID appears on two or more sites: measure it, do not assume.
Write two functions. partition_key(area, origin, top_site) must return the two-part key (top_site, origin) for every storage area. linked_areas(sightings) takes a list of (top_site, area, id_seen) tuples and returns a set of the areas where the same id_seen appears on two or more different top-level sites. Then build engine = MiniEngine(partition_key), run report = run_tracker(engine), and set linked = linked_areas(report["sightings"]). It should print an empty set.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.