Syllabus Lesson 41 of 156 · Inside a tracker (in the practice world)
Inside a tracker (in the practice world)

Switch on partitioned storage

Why it matters Partitioned storage is one of the biggest privacy changes browsers have made. In this lab you measure, with numbers, how much it shrinks what AdCo can join together.

Back to the wristband. Imagine every stall now hands you its own wristband, and only its own. The issuing company still sees each stall, but it can no longer tell that the person at stall one is the person at stall two.

Partitioned storage: the browser keeps a separate cookie jar (and separate storage) for each top-level site, meaning the site shown in the address bar. AdCo's cookie on ShopA lives in ShopA's jar; on ShopB it lives in ShopB's jar. They never meet.

Firefox has done this by default since 2022 (Total Cookie Protection), Safari blocks third-party cookies by default, and Chrome keeps third-party cookies by default but supports partitioned cookies that sites opt into (checked October 2026). In the practice world you can flip each of these on with a switch:

practice_log(world, pages)                                   # one shared jar
practice_log(world, pages, partitioned=True)                 # a jar per shop
practice_log(world, pages, block_third_party_cookies=True)   # no third-party cookies

Measuring "how far it can see"

A simple measure of a tracker's reach is the widest profile: the largest number of different sites joined under any one ID. With one shared jar, Sam's ID joins all three shops, so the widest profile is 3. With third-party cookies blocked, AdCo's cookie is never sent at all, so there are no profiles and the widest is 0.

sizes = [len(sites) for sites in {"a": {"x", "y"}, "b": {"x"}}.values()]
print(sizes)        # [2, 1]: one size per ID
print(max(sizes))   # 2

The square brackets build a list with a short loop inside (a list comprehension): "the length of each set". You can also write an ordinary for loop that keeps the biggest size seen; both are fine.

Use the same join_by_cookie from the last lesson (it is in the starter), then write widest and measure three browsers.

max() with a default: max(values, default=0) returns the largest value, or 0 if there are none. Handy when a profile list might be empty.

In other languages

JavaScript:

function widest(profiles) {
  let best = 0;
  for (const sites of profiles.values()) best = Math.max(best, sites.size);
  return best;
}

Rust:

fn widest(profiles: &HashMap<String, HashSet<String>>) -> usize {
    profiles.values().map(|s| s.len()).max().unwrap_or(0)
}

Recap

  • Partitioning gives each top-level site its own jar, so one cookie can no longer join sites.
  • The widest profile drops from 3 sites to 1 with partitioning, and to 0 when third-party cookies are blocked.
  • Trackers then try other tricks, which the next two lessons cover.
Your turn

Write widest(profiles): the largest number of sites under any one ID (0 if there are none). Then, for the world with seed=7 and Sam's first 12 pages, set before (normal browser), after (partitioned=True) and blocked (block_third_party_cookies=True), each to the widest profile from that log.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output