Syllabus Lesson 77 of 156 · Privacy by design
Privacy by design

Analytics without tracking people

Why it matters Every site owner wants to know which pages people read. The usual way to find out gives each visitor an ID and sends their whole trail to another company. You can answer the owner's real questions, "how many visits, which pages, from where", without following anyone. This lesson shows how, using our own site as the example, weak spots and all.

A museum wants to know how busy each room is. One way is to follow every visitor with a clipboard, noting their face and every room they enter. The other is a clicker at each door. The clicker answers "how busy is room 3 on Tuesdays" just as well, and it knows nothing about anyone.

Aggregate count: a total, such as "the cookies lesson was opened 412 times on Monday", kept instead of a row per person. Once data is aggregated, there is no individual left in it to track.

The privacy-friendly recipe

  1. First party only. The page sends one small request to your own server. No third-party script, so no other company sees the visit.
  2. No cookies and no stored IDs. Nothing is written to the visitor's browser, so there is nothing to follow them with, and nothing to ask consent for.
  3. Send only what answers a question. The page path, the referring site (not the full address), a rough screen size bucket, a language code.
  4. Count unique visitors with a daily salted hash. You met salting in Leg 2. Here the salt is a random secret that changes every day and is then thrown away.
  5. Aggregate, then forget. Keep daily totals; delete the raw rows after a short time.

The daily salt, in code

import hashlib, secrets

salt_of_the_day = {}          # day -> random secret, deleted when the day is over

def visitor_key(ip, user_agent, day):
    salt = salt_of_the_day.setdefault(day, secrets.token_bytes(16))
    text = (ip + "|" + user_agent).encode()
    return hashlib.sha256(salt + text).hexdigest()[:16]

# At midnight: del salt_of_the_day[yesterday]

Within one day, the same visitor gets the same key, so "unique visitors today" is just the number of different keys. Tomorrow, the salt is new, so the same person gets an unrelated key and days cannot be linked. Once yesterday's salt is deleted, nobody, not even you, can recompute yesterday's keys from an IP address.

Case study: floati.life's own counter (checked October 2026)

Here is what this site's counter really does, read from its code.

  • The page's own script sends one POST to floati.life's own /api/track/. No third-party analytics, no cookies, no ID stored in the browser.
  • It sends the page path, the referring site's host name only (and nothing when you came from floati.life itself), the window width and the language code such as en. Course pages also send the name of a step, such as "passed an exercise".
  • Before sending, the script cuts the private part off certificate links, so an owner's secret link never reaches the log. That is link hygiene from the last module, in practice.
  • The server adds the user agent, a "looks like a bot" flag, the device type and a country looked up from the IP address. The IP address itself is not stored with page views.
  • For unique visitors it stores a visitor hash: SHA-256 of the IP address and user agent, cut to 16 hex characters.
  • Only the owner sees the totals, on a PIN-protected page.

Where it falls short, honestly. That visitor hash has no salt. So the same visitor gets the same hash every day, which lets visits be linked across days. And someone holding the database could guess a user agent and try IPv4 addresses one by one (there are about 4.3 billion, which a computer can work through). The full user-agent text is also kept with each row. The fixes are the recipe above: a daily salt that is then deleted, and keeping only the browser family instead of the full user agent. Writing this down is part of the job: an honest audit of your own site is worth more than a perfect-sounding one.

Small numbers need care

Even totals can point at a person. "One visitor from a small town read the debt-help page" might identify them. Hide or round any count below a threshold (for example, show "fewer than 5") before anyone sees the report.

A common misconception: "hashing the IP address anonymises it". Without a secret salt that is later deleted, a hash of an IP is just a different spelling of the same IP, because every possible address can be hashed and matched.

In other languages
// JavaScript (Node)
const key = crypto.createHash("sha256")
  .update(dailySalt).update(ip + "|" + ua).digest("hex").slice(0, 16);
// Go
h := sha256.New()
h.Write(dailySalt); h.Write([]byte(ip + "|" + ua))
key := hex.EncodeToString(h.Sum(nil))[:16]
// PHP
$key = substr(hash('sha256', $dailySalt . $ip . '|' . $ua), 0, 16);

Recap

  • First party, no cookies, only the fields that answer a question, then aggregate and delete.
  • A daily salt that is thrown away counts unique visitors per day without linking days.
  • Our own counter is cookie-free and first party, but its unsalted visitor hash is a weak spot worth fixing.
  • Hide very small counts before sharing a report.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output