Clean links and catch bounces
Why it matters When cookies are partitioned, trackers carry your ID somewhere else: in the link itself, or by bouncing you through their own site for a split second. A blocker that only looks at scripts and pixels misses both. This lesson closes those two gaps.
Two tricks, one idea
Imagine a friend passing your note to someone across the room, but first writing their own initials on it. That is link decoration: an ID added to the address you are going to, like ?adco_uid=7f3a....
Now imagine every note having to go via the friend's desk first. That is a bounce: you click a link to ShopB, but it really goes to adco.example/click?to=..., which reads its cookie, notes the click, and sends you on. You barely notice.
Link decoration: extra parameters added to a URL to carry an identifier from one site to another, such as utm_ tags, fbclid, gclid or a tracker's own ID.
Bounce tracking: sending a user through a tracker's site in a quick redirect, so the tracker gets a first-party visit, can read and set its own cookies, and learns where the user came from and is going.
Redirect: a server's answer that says "not here, go to this other address" (status 301, 302, 303, 307 or 308 with a Location header). The browser follows it by itself.
You saw both tricks in AdCo's log in the tracker module (lessons 42 and 43), and you wrote a full link cleaner in lesson 37 (URLs and link cleaning). Here the cleaner is given in a short form; the new skill is where and when to use it.
Defence 1: clean every navigation, hops included
A navigation is a request to load a whole page: request.kind is "document" (or "form" for a form post). Our practice browser asks your blocker about every hop of a redirect chain, not just the first. So if AdCo redirects you to shopb.example/?adco_uid=..., your blocker gets a chance to answer with the cleaned address instead, and ShopB never sees the ID.
Return the new URL as a string and the browser uses it instead (the log marks it cleaned). Return "allow" when nothing changed.
Defence 2: find bouncers, then skip them
Every Visit has a chain: the list of addresses it passed through, last one being where you landed. A bouncer has a telling pattern:
- it appears in a chain before the last hop, on a different site from the destination, and
- it is never the destination itself. People do not visit
adco.exampleon purpose.
visit.chain
# ['https://adco.example/click?to=https%3A%2F%2Fshopb.example%2F',
# 'https://shopb.example/']
# adco.example is in the middle, shopb.example is where you landedOnce a site is known as a bouncer, the blocker can do better than cleaning: when a navigation goes to a bouncer and one of its query values is itself a web address, send the user straight there. The tracker never gets the visit at all.
What browsers do (checked October 2026)
Safari has detected and cleared storage for bounce trackers since 2018. Firefox purges known trackers' storage daily by default and has fuller bounce tracking protection in Strict mode. Chrome applies bounce tracking mitigations only for people who block third-party cookies. Several browsers also strip well-known decoration parameters, such as fbclid, in private modes or strict settings.
A common misconception
"Just strip every parameter." That breaks things: ?page=2, a search term or a password-reset token is often exactly what the page needs. Strip known tracking parameters only, and keep the rest. Real cleaners also keep an allowlist for login and payment flows.
Try it Hover over a sponsored link in a search engine or a social site and look at the address shown at the bottom of the window. If it starts with the platform's own domain and carries your destination inside it, you are looking at a bounce link.
In other languages
JavaScript:
function cleanUrl(url) {
const u = new URL(url);
for (const key of [...u.searchParams.keys()]) {
const k = key.toLowerCase();
if (k.startsWith("utm_") || TRACKING.has(k)) u.searchParams.delete(key);
}
return u.toString();
}
function findBouncers(visits) {
const middle = new Set(), dest = new Set();
for (const v of visits) {
const final = siteOf(v.url);
dest.add(final);
v.chain.slice(0, -1).forEach(h => { if (siteOf(h) !== final) middle.add(siteOf(h)); });
}
return [...middle].filter(s => !dest.has(s));
}PHP:
function clean_url(string $url): string {
$p = parse_url($url);
parse_str($p['query'] ?? '', $q);
foreach (array_keys($q) as $k) {
if (str_starts_with(strtolower($k), 'utm_') || in_array(strtolower($k), TRACKING)) unset($q[$k]);
}
return $p['scheme'] . '://' . $p['host'] . ($p['path'] ?? '') . ($q ? '?' . http_build_query($q) : '');
}Recap
- Clean decoration on every navigation, including each redirect hop.
- A bouncer sits in the middle of redirect chains and is never a real destination.
- Known bouncers can be skipped by going straight to the address they carry.
- Strip known tracking parameters only; other parameters keep sites working.
clean_url is given. Write find_bouncers(visits): the set of sites that appear in a visit's chain before its last hop, on a different site from that visit's final URL, and that are never the final site of any visit. Then write make_blocker(bouncers), which returns a function blocker(request): return "allow" for requests whose kind is not "document" or "form"; for a navigation to a site in bouncers that carries a query value starting with http:// or https://, return clean_url of that value; otherwise return clean_url(request.url) if it differs from request.url, else "allow".
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.