Direct and indirect identifiers
The useful split is not sensitive versus harmless, it is direct versus indirect. Direct identifiers name a person by themselves: full name, email address, phone number, government ID. Indirect identifiers single someone out once combined: an IP address, a device ID, a session ID, a postcode plus a birth date, or a user ID that maps to a row in your database.
Analytics runs almost entirely on indirect identifiers, which is why “we don’t collect PII, we only use IDs” is not the defence it sounds like. If the ID can be resolved back to a person, by you or by anyone with the other half of the mapping, it is personal data in every regime that matters.
Three regimes, three words
The terminology shifts by jurisdiction, and the scope widens as it goes:
- PII is the traditional US term and the narrowest, historically focused on data that identifies someone directly.
- Personal data is the GDPR’s term: any information relating to an identified or identifiable natural person, explicitly including online identifiers.
- Personal information is the CCPA’s term and the broadest, naming IP addresses, device identifiers, and “internet or other electronic network activity information” such as browsing history, and extending to households.
India’s DPDP Act uses “digital personal data” and lands close to the GDPR. If you operate across markets, engineer to the widest definition that applies to you rather than maintaining three mental models.
How analytics collects PII by accident
Deliberate collection is easy to govern, because someone chose it. The leaks are what cause incidents:
- Query strings. Password-reset links, magic links, and OAuth callbacks routinely carry a token or an email address, and every pageview event captures the URL.
- Path segments.
/invoices/user@example.comor/u/42/settingsputs an identifier straight into the event. - Page titles. A title can imply a diagnosis, a financial position, or a legal matter that no field ever recorded. California’s largest CCPA settlement to date turned on exactly this.
- Event properties. A well-meaning
track('signup', { email })puts a direct identifier on every row, forever. - Free-text fields. Anything a user typed can contain anything at all.
Auditing a tracking plan before it ships is far cheaper than purging a column afterwards. Our free PII event auditor flags likely identifiers in an event schema, entirely in the browser.
Minimisation is the only durable control
Every other control is a mitigation of a decision you already made. Data you never collected needs no retention policy, appears in no breach, and takes no work to erase on request. Start from the questions you actually have to answer, capture what those need, and drop the rest, especially anything you are keeping because it might be useful later.
How Pug handles it
Pug starts cookieless: nothing is written to the device and identity is
derived server-side from a salt that is deleted daily, so events are pseudonymous until you choose otherwise. The
direct identifiers on a profile are the traits you pass to identify(), so the field list is
yours to keep short.
For the most common leak, the Web SDK redacts sensitive URL parameters before the event is sent. The default
list covers token, access_token, refresh_token, id_token
and any key ending in _token, plus code, auth, authorization,
api_key, apikey, secret, sig, signature,
password, passwd, pwd, otp, email,
phone and ssn. Values are replaced with redacted, and the list is
configurable:
init(projectId, { apiKey, redactUrlParams: ['invite', 'ref_code'] })
Redaction applies to the captured URL, the referrer, and a form’s action, in both the query string and the
fragment, and it runs before beforeSend so a hook can mask more. Passing an array
replaces the default list rather than extending it, and the _token suffix rule rides the
default list only, so a replacement has to name every param it wants. Note also that autocaptured form events
carry the form’s id, name, and scrubbed action URL, never what was typed into a field.
When someone asks to be forgotten, erasure is a server-side operation against the store you hold: one API call hard-deletes a data subject’s events, sessions, aliases, and profile, and returns a request id you can poll to confirm it finished. Self-hosting takes it further, since the data never reaches anyone else at all, see privacy-first analytics for the wider picture.